├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── php.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── apigee_api_catalog.info.yml ├── apigee_api_catalog.install ├── apigee_api_catalog.libraries.yml ├── apigee_api_catalog.links.action.yml ├── apigee_api_catalog.links.menu.yml ├── apigee_api_catalog.links.task.yml ├── apigee_api_catalog.module ├── apigee_api_catalog.routing.yml ├── apigee_api_catalog.services.yml ├── codecov.yml ├── composer.json ├── config ├── install │ ├── core.base_field_override.node.apidoc.title.yml │ ├── core.entity_form_display.node.apidoc.default.yml │ ├── core.entity_view_display.node.apidoc.default.yml │ ├── field.field.node.apidoc.body.yml │ ├── field.field.node.apidoc.field_api_product.yml │ ├── field.field.node.apidoc.field_apidoc_fetched_timestamp.yml │ ├── field.field.node.apidoc.field_apidoc_file_link.yml │ ├── field.field.node.apidoc.field_apidoc_spec.yml │ ├── field.field.node.apidoc.field_apidoc_spec_file_source.yml │ ├── field.field.node.apidoc.field_apidoc_spec_md5.yml │ ├── field.storage.node.field_api_product.yml │ ├── field.storage.node.field_apidoc_fetched_timestamp.yml │ ├── field.storage.node.field_apidoc_file_link.yml │ ├── field.storage.node.field_apidoc_spec.yml │ ├── field.storage.node.field_apidoc_spec_file_source.yml │ ├── field.storage.node.field_apidoc_spec_md5.yml │ └── node.type.apidoc.yml └── optional │ ├── views.view.api_catalog_admin.yml │ └── views.view.apigee_api_catalog.yml ├── js └── smartdocs_integration.js ├── modules ├── apigee_asyncapi_doc │ ├── apigee_asyncapi_doc.info.yml │ ├── apigee_asyncapi_doc.install │ ├── apigee_asyncapi_doc.libraries.yml │ ├── apigee_asyncapi_doc.links.action.yml │ ├── apigee_asyncapi_doc.module │ ├── apigee_asyncapi_doc.routing.yml │ ├── apigee_asyncapi_doc.services.yml │ ├── config │ │ ├── install │ │ │ ├── field.field.node.asyncapi_doc.body.yml │ │ │ ├── field.field.node.asyncapi_doc.field_api_product.yml │ │ │ ├── field.field.node.asyncapi_doc.field_asyncapi_spec.yml │ │ │ ├── field.field.node.asyncapi_doc.field_asyncapi_spec_file_link.yml │ │ │ ├── field.field.node.asyncapi_doc.field_asyncapi_spec_source_type.yml │ │ │ ├── field.storage.node.field_asyncapi_spec.yml │ │ │ ├── field.storage.node.field_asyncapi_spec_file_link.yml │ │ │ ├── field.storage.node.field_asyncapi_spec_source_type.yml │ │ │ └── node.type.asyncapi_doc.yml │ │ └── optional │ │ │ ├── core.entity_form_display.node.asyncapi_doc.default.yml │ │ │ └── core.entity_view_display.node.asyncapi_doc.default.yml │ ├── css │ │ └── apigee-asyncapi-doc.css │ ├── src │ │ ├── ApigeeAsyncapiDocBreadcrumbBuilder.php │ │ ├── ApigeeAsyncapiDocUninstallValidator.php │ │ ├── EventSubscriber │ │ │ └── ApigeeAsyncapiDocSubscriber.php │ │ ├── Form │ │ │ └── ConfirmRemoveViewsReferencesForm.php │ │ └── Plugin │ │ │ └── Field │ │ │ └── FieldFormatter │ │ │ └── AsyncapiFormatter.php │ └── templates │ │ └── apigee-asyncapi-doc-file-link-field-item.html.twig ├── apigee_freeform_doc │ ├── apigee_freeform_doc.info.yml │ ├── apigee_freeform_doc.install │ ├── apigee_freeform_doc.links.action.yml │ ├── apigee_freeform_doc.routing.yml │ ├── apigee_freeform_doc.services.yml │ ├── config │ │ ├── install │ │ │ ├── field.field.node.freeform_doc.body.yml │ │ │ ├── field.field.node.freeform_doc.field_api_product.yml │ │ │ ├── field.field.node.freeform_doc.field_freeform_spec_doc.yml │ │ │ ├── field.storage.node.field_freeform_spec_doc.yml │ │ │ └── node.type.freeform_doc.yml │ │ └── optional │ │ │ ├── core.entity_form_display.node.freeform_doc.default.yml │ │ │ └── core.entity_view_display.node.freeform_doc.default.yml │ └── src │ │ ├── ApigeeFreeformDocBreadcrumbBuilder.php │ │ ├── ApigeeFreeformDocUninstallValidator.php │ │ ├── EventSubscriber │ │ └── ApigeeFreeformDocSubscriber.php │ │ └── Form │ │ └── ConfirmRemoveViewsReferencesForm.php └── apigee_graphql_doc │ ├── apigee_graphql_doc.info.yml │ ├── apigee_graphql_doc.install │ ├── apigee_graphql_doc.libraries.yml │ ├── apigee_graphql_doc.links.action.yml │ ├── apigee_graphql_doc.module │ ├── apigee_graphql_doc.routing.yml │ ├── apigee_graphql_doc.services.yml │ ├── config │ ├── install │ │ ├── field.field.node.graphql_doc.body.yml │ │ ├── field.field.node.graphql_doc.field_api_product.yml │ │ ├── field.field.node.graphql_doc.field_graphql_spec.yml │ │ ├── field.field.node.graphql_doc.field_graphql_spec_file_link.yml │ │ ├── field.field.node.graphql_doc.field_graphql_spec_source_type.yml │ │ ├── field.storage.node.field_graphql_spec.yml │ │ ├── field.storage.node.field_graphql_spec_file_link.yml │ │ ├── field.storage.node.field_graphql_spec_source_type.yml │ │ └── node.type.graphql_doc.yml │ └── optional │ │ ├── core.entity_form_display.node.graphql_doc.default.yml │ │ └── core.entity_view_display.node.graphql_doc.default.yml │ ├── css │ └── apigee-graphql-doc.css │ ├── js │ └── apigee-graphql-doc.js │ ├── src │ ├── ApigeeGraphqlDocBreadcrumbBuilder.php │ ├── ApigeeGraphqlDocUninstallValidator.php │ ├── Controller │ │ └── ApigeeGraphqlServerController.php │ ├── EventSubscriber │ │ └── ApigeeGraphqlDocSubscriber.php │ ├── Form │ │ └── ConfirmRemoveViewsReferencesForm.php │ └── Plugin │ │ └── Field │ │ └── FieldFormatter │ │ └── GraphiqlFormatter.php │ └── templates │ └── apigee-graphql-doc-file-link-field-item.html.twig ├── phpcs.xml.dist ├── phpunit.core.xml.dist ├── src ├── ApigeeApiCatalogBreadcrumbBuilder.php ├── Entity │ └── Form │ │ └── ApiDocReimportSpecForm.php ├── EventSubscriber │ └── PageNotFoundEventSubscriber.php ├── Plugin │ ├── Field │ │ └── FieldFormatter │ │ │ └── SmartDocsFormatter.php │ └── Validation │ │ └── Constraint │ │ ├── ApiDocFileLinkConstraint.php │ │ └── ApiDocFileLinkConstraintValidator.php ├── SpecFetcher.php ├── SpecFetcherInterface.php └── UpdateService.php └── tests └── src ├── Functional ├── ApiDocsAdminTest.php ├── ApiDocsBreadcrumbTest.php ├── ApiDocsJsonApi.php └── SmartdocRoutingTest.php └── Kernel └── ApidocEntityTest.php /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | ## Description 10 | A clear and concise description of what the bug is. 11 | 12 | ## Steps to Reproduce 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 18 | ## Actual Behavior 19 | What happened after the steps to reproduce. 20 | 21 | ## Expected Behavior 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## Screenshots 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | ## Notes 28 | Add any other context about the problem here. 29 | 30 | ## Version Info 31 | This can be the version you can see on admin/modules in Drupal or 32 | the output of this command: `composer show`. Add Drupal core and 33 | other version information if needed. 34 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you would like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: test_and_lint 2 | env: 3 | SIMPLETEST_DB: "sqlite://localhost//tmp/test.sqlite" 4 | SIMPLETEST_BASE_URL: "http://127.0.0.1:8000" 5 | APIGEE_EDGE_AUTH_TYPE: ${{ secrets.APIGEE_EDGE_AUTH_TYPE }} 6 | APIGEE_EDGE_ORGANIZATION: ${{ secrets.APIGEE_EDGE_ORGANIZATION }} 7 | APIGEE_EDGE_USERNAME: ${{ secrets.APIGEE_EDGE_USERNAME }} 8 | APIGEE_EDGE_PASSWORD: ${{ secrets.APIGEE_EDGE_PASSWORD }} 9 | APIGEE_EDGE_INSTANCE_TYPE: ${{ secrets.APIGEE_EDGE_INSTANCE_TYPE }} 10 | APIGEE_INTEGRATION_ENABLE: ${{ secrets.APIGEE_INTEGRATION_ENABLE }} 11 | APIGEE_EDGE_ENDPOINT: ${{ secrets.APIGEE_EDGE_ENDPOINT }} 12 | BROWSERTEST_OUTPUT_DIRECTORY: "sites/simpletest/browser_output" 13 | BROWSERTEST_OUTPUT_BASE_URL: "" 14 | MINK_DRIVER_ARGS_WEBDRIVER: '["chrome", { "chromeOptions": { "w3c": false } }, "http://127.0.0.1:9515/wd/hub"]' 15 | 16 | on: 17 | push: 18 | pull_request_target: 19 | schedule: 20 | - cron: '30 01 * * *' 21 | 22 | jobs: 23 | build: 24 | 25 | runs-on: ubuntu-24.04 26 | 27 | name: "PHP ${{ matrix.php-version }} | Drupal ${{ matrix.drupal-core }}" 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | php-version: 32 | - "8.1" 33 | - "8.2" 34 | - "8.3" 35 | drupal-core: 36 | # Should update the following as the minimum supported version from Drupal.org 37 | - "10.3.x" 38 | - "11.1.x" 39 | exclude: 40 | - php-version: "8.1" 41 | drupal-core: "11.1.x" 42 | - php-version: "8.2" 43 | drupal-core: "11.1.x" 44 | 45 | steps: 46 | 47 | - name: Disable deprecations check for PR 48 | if: github.event_name != 'schedule' 49 | run: | 50 | echo "Disable deprecations check" 51 | echo "SYMFONY_DEPRECATIONS_HELPER=disabled" >> $GITHUB_ENV 52 | 53 | - name: "Install PHP" 54 | uses: "shivammathur/setup-php@v2" 55 | with: 56 | coverage: "xdebug" 57 | php-version: "${{ matrix.php-version }}" 58 | tools: composer:v2 59 | extensions: date, dom, filter, hash, json, curl, libxml, mbstring, zip, pdo, sqlite3, mysqli, mysql, pdo_mysql, bcmath, gd, exif, iconv, opcache, imagick, openssl 60 | 61 | - name: Checkout Drupal core 62 | run: | 63 | git clone --depth 1 --branch ${{ matrix.drupal-core }} https://github.com/drupal/drupal.git drupal 64 | mkdir -p drupal/modules/contrib/apigee_api_catalog 65 | mkdir -p drupal/sites/simpletest/browser_output 66 | 67 | - name: Checkout apigee_api_catalog module 68 | uses: actions/checkout@v4 69 | with: 70 | path: drupal/modules/contrib/apigee_api_catalog 71 | 72 | - name: "Allow plugins and dev dependencies" 73 | run: | 74 | cd drupal 75 | composer config --no-plugins allow-plugins.composer/installers true 76 | composer config --no-plugins allow-plugins.drupal/core-composer-scaffold true 77 | composer config --no-plugins allow-plugins.drupal/core-project-message true 78 | composer config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true 79 | composer config --no-plugins allow-plugins.wikimedia/composer-merge-plugin true 80 | composer config --no-plugins allow-plugins.composer/package-versions-deprecated true 81 | composer config --no-plugins allow-plugins.cweagans/composer-patches true 82 | composer config --no-plugins allow-plugins.php-http/discovery true 83 | composer config minimum-stability dev 84 | composer require wikimedia/composer-merge-plugin 85 | composer config --json extra.merge-plugin.require '["modules/contrib/apigee_api_catalog/composer.json"]' 86 | composer config platform.php ${{ matrix.php-version }} 87 | composer config --json extra.patches."drupal/core" '{ "Support entities that are neither content nor config entities": "https://www.drupal.org/files/issues/2020-12-02/3042467-50.patch"}' 88 | composer update --with-all-dependencies 89 | composer require --dev phpspec/prophecy-phpunit:^2 90 | 91 | # Install drupal using minimal installation profile and enable the module. 92 | - name: Install Drupal 93 | run: | 94 | cd drupal 95 | php -d sendmail_path=$(which true); vendor/bin/drush --yes -v site-install minimal --db-url="$SIMPLETEST_DB" 96 | vendor/bin/drush rs 8000 & 97 | 98 | - name: "PHPCS" 99 | run: | 100 | cd drupal 101 | cp modules/contrib/apigee_api_catalog/phpcs.xml.dist . 102 | vendor/bin/phpcs --standard=./phpcs.xml.dist modules/contrib/apigee_api_catalog -p -s -n --colors 103 | composer show > composer.show.txt 104 | 105 | - name: Artifacts 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: composer-${{ matrix.php-version }}-${{ matrix.drupal-core }}-artifact 109 | path: drupal/composer.* 110 | 111 | - uses: nanasess/setup-chromedriver@v2 112 | 113 | - run: | 114 | export DISPLAY=:99 115 | chromedriver --url-base=/wd/hub & 116 | sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional 117 | 118 | - name: "Run PHPUnit Tests" 119 | if: ${{ matrix.drupal-core != '10.3.x' || matrix.php-version != '8.2' }} 120 | run: | 121 | cd drupal 122 | vendor/bin/phpunit -c core --color --group apigee_api_catalog --testsuite unit,kernel,functional,functional-javascript modules/contrib/apigee_api_catalog 123 | 124 | - name: "Run PHPUnit Tests with Code Coverage" 125 | if: ${{ matrix.drupal-core == '10.3.x' && matrix.php-version == '8.2' }} 126 | run: | 127 | cd drupal 128 | cp modules/contrib/apigee_api_catalog/phpunit.core.xml.dist core/phpunit.xml 129 | vendor/bin/phpunit -c core --color --group apigee_api_catalog --testsuite unit,kernel,functional,functional-javascript --coverage-clover /tmp/coverage.xml modules/contrib/apigee_api_catalog 130 | 131 | - name: Artifacts 132 | if: failure() 133 | uses: actions/upload-artifact@v4 134 | with: 135 | name: browser-output-${{ matrix.php-version }}-${{ matrix.drupal-core }}-artifact 136 | path: drupal/sites/simpletest/browser_output/* 137 | 138 | - name: Upload Coverage to Codecov 139 | if: ${{ matrix.drupal-core == '10.3.x' && matrix.php-version == '8.2' }} 140 | uses: codecov/codecov-action@v4 141 | with: 142 | token: ${{ secrets.CODECOV_TOKEN }} 143 | files: /tmp/coverage.xml 144 | name: codecov-umbrella 145 | fail_ci_if_error: true 146 | verbose: true 147 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to Chris Novak [chrisnovak@google.com], the 73 | Project Steward for Apigee API Catalog module for Drupal. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apigee API Catalog for Drupal 2 | 3 | Render OpenAPI specs as documentation to your API developers. 4 | 5 | ## Overview 6 | 7 | When you enable this module, it creates a new content type named 8 | "API Doc". You can add new API docs under __Content > API catalog__ in the admin menu. 9 | 10 | Once added, the API name and description for each API Doc will be displayed in the 11 | "APIs" menu item on the site using a Drupal view. 12 | 13 | The OpenAPI spec by default is rendered using Apigee's SmartDocs custom field formatter. 14 | 15 | The OpenAPI spec can be directly uploaded as a file, or associated to a source location 16 | such as Apigee Edge or a URL. A "Re-import OpenAPI spec" operation is available per 17 | API Doc to re-import the spec file when source location changes. 18 | 19 | The OpenAPI spec by default is shown on the API Doc detail page by default. 20 | To render the OpenAPI spec using Swagger UI: 21 | 22 | 1. Install and enable the [Swagger UI Field Formatter](https://www.drupal.org/project/swagger_ui_formatter) module. 23 | 2. Install the Swagger UI JS library as documented [on the module page](https://www.drupal.org/project/swagger_ui_formatter). 24 | 3. Go to __Configuration > API docs > Manage display__ in the admin menu. 25 | 4. Change "OpenAPI specification" field format to use the Swagger UI field formatter. 26 | 27 | The API Doc is an node type, you can configure it at __Structure > Content types > API Doc__ in the admin 28 | menu. 29 | 30 | The "/apis" page ("APIs" menu link) is a view. You can modify it by editing the "API Catalog" view 31 | under __Structure > Views__ in the admin menu. 32 | 33 | ## API Docs RBAC (Role based access control) 34 | 35 | _API Docs_ are a node type, so any node access control module from contrib will work to restrict access and play well 36 | with views. 37 | 38 | To set up RBAC, we recommend ["Permissions by term"](https://www.drupal.org/project/permissions_by_term), which can 39 | cover the most frequent scenarios. Refer to the online documentation (to be updated as part of 40 | [#81](https://github.com/apigee/apigee-api-catalog-drupal/issues/81)) for a "How to" guide on setting up the RBAC: 41 | https://www.drupal.org/node/3058344 42 | 43 | ## Planned Features 44 | 45 | - Integration with Apigee API Products. 46 | - Add visual notifications when source URL specs have changed on the API Doc admin screen 47 | 48 | ### Known issues 49 | 50 | - The Apigee SmartDocs formatter can only render one OpenAPI spec per page and the URL pattern 51 | must match `/api/{entityId}`. This means that the formatter will probably not work correctly if 52 | you modify the default API Docs view or try to use this file Formatter on other node types or entities. 53 | 54 | ## Installing 55 | 56 | This module must be installed on a Drupal site that is managed by Composer. Drupal.org has documentation on how to 57 | [use Composer to manage Drupal site dependencies](https://www.drupal.org/docs/develop/using-composer/using-composer-to-manage-drupal-site-dependencies) 58 | to get you started quickly. 59 | 60 | 1. Install the module using [Composer](https://getcomposer.org/). 61 | Composer will download the this module and all its dependencies. 62 | **Note**: Composer must be executed at the root of your Drupal installation. 63 | For example: 64 | ``` 65 | cd /path/to/drupal/root 66 | composer require drupal/apigee_api_catalog 67 | ``` 68 | For more information about installing contributed modules using Composer, read 69 | [how to download contributed modules and themes using Composer](https://www.drupal.org/docs/develop/using-composer/using-composer-to-manage-drupal-site-dependencies#managing-contributed). 70 | 2. Choose **Extend** in the Drupal administration menu. 71 | 3. Select the **Apigee API catalog** module. 72 | 4. Choose **Install**. 73 | 74 | ## Development 75 | 76 | Development is happening in our [GitHub repository](https://github.com/apigee/apigee-api-catalog-drupal). The Drupal.org issue 77 | queue is disabled; we use the [Github issue queue](https://github.com/apigee/apigee-api-catalog-drupal) to coordinate 78 | development. See [CONTRIBUTING.md] for more information on contributing through development. 79 | 80 | ## Support 81 | 82 | This project, which integrates Drupal 8 with Apigee Edge, is supported by Google. 83 | -------------------------------------------------------------------------------- /apigee_api_catalog.info.yml: -------------------------------------------------------------------------------- 1 | name: 'Apigee API Catalog' 2 | type: module 3 | description: 'Display OpenAPI documentation of your APIs to your developers.' 4 | core_version_requirement: ^10 || ^11 5 | package: 'Apigee' 6 | dependencies: 7 | - drupal:text 8 | - drupal:entity 9 | - drupal:file 10 | - drupal:user 11 | - drupal:node 12 | - drupal:path 13 | - drupal:options 14 | - drupal:file_link 15 | - drupal:apigee_edge 16 | -------------------------------------------------------------------------------- /apigee_api_catalog.install: -------------------------------------------------------------------------------- 1 | moduleExists('apigee_edge_apidocs')) { 32 | return [ 33 | 'apigee_api_catalog_module_conflict' => [ 34 | 'title' => t('Apigee API Catalog'), 35 | 'description' => t('The "Apigee Edge API Docs" module is currently installed. It is deprecated and should be uninstalled before installing the Apigee API Catalog module.'), 36 | 'severity' => REQUIREMENT_ERROR, 37 | ], 38 | ]; 39 | } 40 | 41 | return []; 42 | } 43 | 44 | /** 45 | * Update the length of the name field. 46 | */ 47 | function apigee_api_catalog_update_8701() { 48 | $entity_type_id = 'apidoc'; 49 | $field_name = 'name'; 50 | $field_length = 255; 51 | $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); 52 | $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); 53 | $entity_type = $definition_update_manager->getEntityType($entity_type_id); 54 | 55 | // Update the field storage definition. 56 | /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions */ 57 | $field_storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); 58 | $field_storage_definitions[$field_name]->setSetting('max_length', $field_length); 59 | $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox); 60 | 61 | $definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id); 62 | $data_table = $definition->getDataTable(); 63 | $revision_data_table = $definition->getRevisionDataTable(); 64 | 65 | $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql'); 66 | $schema_key = "$entity_type_id.field_schema_data.$field_name"; 67 | if ($field_schema_data = $entity_storage_schema_sql->get($schema_key)) { 68 | // Update storage schema for data table. 69 | $field_schema_data[$data_table]['fields'][$field_name]['length'] = $field_length; 70 | 71 | // Update storage schema for the revision table. 72 | if ($revision_data_table) { 73 | $field_schema_data[$revision_data_table]['fields'][$field_name]['length'] = $field_length; 74 | } 75 | 76 | $entity_storage_schema_sql->set($schema_key, $field_schema_data); 77 | 78 | // Update the base database field. 79 | $schema = \Drupal::database()->schema(); 80 | $schema->changeField($data_table, $field_name, $field_name, $field_schema_data[$data_table]['fields'][$field_name]); 81 | 82 | // Update schema for the revision table. 83 | if ($revision_data_table) { 84 | $schema->changeField($revision_data_table, $field_name, $field_name, $field_schema_data[$revision_data_table]['fields'][$field_name]); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Set module schema to 8801. 91 | */ 92 | function apigee_api_catalog_update_8801() { 93 | // Empty - only needed to set the 2.x module schema number. 94 | } 95 | 96 | /** 97 | * Create API Doc node type and fields if updating from 1.x. 98 | */ 99 | function apigee_api_catalog_update_8802() { 100 | return \Drupal::service('apigee_api_catalog.updates')->update8802(); 101 | } 102 | 103 | /** 104 | * Recreate other fields added to the API Doc entity onto the API Doc node type if updating from 1.x. 105 | */ 106 | function apigee_api_catalog_update_8803() { 107 | return \Drupal::service('apigee_api_catalog.updates')->update8803(); 108 | } 109 | 110 | /** 111 | * Convert API Doc entities to nodes (migrating data) if updating from 1.x. 112 | */ 113 | function apigee_api_catalog_update_8804(&$sandbox) { 114 | return \Drupal::service('apigee_api_catalog.updates')->update8804($sandbox); 115 | } 116 | 117 | /** 118 | * Deprecated step. 119 | */ 120 | function apigee_api_catalog_update_8805() { 121 | // return \Drupal::service('apigee_api_catalog.updates')->update8805(); 122 | } 123 | 124 | /** 125 | * Re-install if needed deprecated API Doc entity definition. 126 | */ 127 | function apigee_api_catalog_update_8806() { 128 | return \Drupal::service('apigee_api_catalog.updates')->update8806(); 129 | } 130 | 131 | /** 132 | * Removed required atttribute for field_apidoc_spec_file_source. 133 | */ 134 | function apigee_api_catalog_update_8807() { 135 | return \Drupal::service('apigee_api_catalog.updates')->update8807(); 136 | } 137 | 138 | /** 139 | * Add API Product field. 140 | */ 141 | function apigee_api_catalog_update_8808() { 142 | return \Drupal::service('apigee_api_catalog.updates')->update8808(); 143 | } 144 | 145 | /** 146 | * Rename API Doc content type as OpenAPI Doc. 147 | */ 148 | function apigee_api_catalog_update_8809() { 149 | return \Drupal::service('apigee_api_catalog.updates')->update8809(); 150 | } 151 | 152 | /** 153 | * Removed yml extension from field_apidoc_file_link and field_apidoc_spec allowed values. 154 | */ 155 | function apigee_api_catalog_update_8810() { 156 | return \Drupal::service('apigee_api_catalog.updates')->update8810(); 157 | } 158 | -------------------------------------------------------------------------------- /apigee_api_catalog.libraries.yml: -------------------------------------------------------------------------------- 1 | # SmartDocs Drupal Angular application. 2 | smartdocs: 3 | version: 1.0.0 4 | license: 5 | name: Proprietary Google Code 6 | gpl-compatible: false 7 | css: 8 | component: 9 | https://fonts.googleapis.com/icon?family=Material+Icons: { type: external } 10 | https://www.gstatic.com/smartdocs/1.1.0/styles.css: { type: external } 11 | js: 12 | https://www.gstatic.com/smartdocs/1.1.0/es2015-polyfills.js: { type: external, minified: true } 13 | https://www.gstatic.com/smartdocs/1.1.0/main.js: { type: external, minified: true } 14 | https://www.gstatic.com/smartdocs/1.1.0/polyfills.js: { type: external, minified: true } 15 | https://www.gstatic.com/smartdocs/1.1.0/runtime.js: { type: external, minified: true } 16 | https://www.gstatic.com/smartdocs/1.1.0/vendor.js: { type: external, minified: true } 17 | 18 | # Integration of SmartDocs Angular application with Drupal, 19 | # passes the OpenAPI spec URL to the SmartDocs ng app. 20 | smartdocs_integration: 21 | version: VERSION 22 | js: 23 | js/smartdocs_integration.js: {} 24 | dependencies: 25 | - core/jquery 26 | - core/drupal 27 | 28 | # Library to parse OpenAPI YAML files to pass to SmartDocs Angular app. 29 | js_yaml: 30 | version: 3.13.1 31 | license: 32 | name: MIT 33 | url: https://github.com/nodeca/js-yaml/blob/master/LICENSE 34 | gpl-compatible: true 35 | js: 36 | https://cdn.jsdelivr.net/npm/js-yaml@3.13.1/dist/js-yaml.min.js: { type: external, minified: true } 37 | -------------------------------------------------------------------------------- /apigee_api_catalog.links.action.yml: -------------------------------------------------------------------------------- 1 | node.apidoc.add_form: 2 | route_name: node.add 3 | title: 'OpenAPI' 4 | route_parameters: 5 | node_type: 'apidoc' 6 | appears_on: 7 | - view.api_catalog_admin.page_1 8 | -------------------------------------------------------------------------------- /apigee_api_catalog.links.menu.yml: -------------------------------------------------------------------------------- 1 | apigee_api_catalog.apidoc.collection: 2 | title: 'API catalog' 3 | route_name: view.api_catalog_admin.page_1 4 | description: 'List of all API documentation.' 5 | parent: system.admin_content 6 | weight: -1 7 | -------------------------------------------------------------------------------- /apigee_api_catalog.links.task.yml: -------------------------------------------------------------------------------- 1 | apidoc.admin: 2 | title: API catalog 3 | route_name: view.api_catalog_admin.page_1 4 | base_route: system.admin_content 5 | weight: -10 6 | 7 | entity.node.apidoc.reimport_spec_form: 8 | route_name: entity.node.reimport_spec_form 9 | base_route: entity.node.canonical 10 | title: 'Re-import OpenAPI spec' 11 | weight: 30 12 | -------------------------------------------------------------------------------- /apigee_api_catalog.module: -------------------------------------------------------------------------------- 1 | ' . t('About') . ''; 41 | $output .= '

' . t('Display documentation of your APIs to your developers using OpenAPI documentation.') . '

'; 42 | return $output; 43 | 44 | default: 45 | } 46 | } 47 | 48 | /** 49 | * Implements hook_entity_type_build(). 50 | */ 51 | function apigee_api_catalog_entity_type_build(array &$entity_types) { 52 | if (isset($entity_types['node'])) { 53 | // Add the reimport form class and link template. The route access check 54 | // makes sure it does not show up for other node types. 55 | $entity_types['node']->setFormClass('reimport_spec', '\Drupal\apigee_api_catalog\Entity\Form\ApiDocReimportSpecForm'); 56 | $entity_types['node']->setLinkTemplate('reimport-spec-form', '/node/{node}/reimport'); 57 | } 58 | } 59 | 60 | /** 61 | * Implements hook_entity_bundle_field_info_alter(). 62 | */ 63 | function apigee_api_catalog_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) { 64 | if ($entity_type->id() == 'node' && $bundle == 'apidoc' && !empty($fields['field_apidoc_file_link'])) { 65 | $fields['field_apidoc_file_link']->addConstraint('ApiDocFileLink', []); 66 | } 67 | } 68 | 69 | /** 70 | * Implements hook_ENTITY_TYPE_presave(). 71 | */ 72 | function apigee_api_catalog_node_presave(EntityInterface $entity) { 73 | /* @var \Drupal\node\NodeInterface $entity */ 74 | 75 | if ($entity->bundle() != 'apidoc') { 76 | return; 77 | } 78 | 79 | // Fetch spec file if using URL. 80 | if ($entity->get('field_apidoc_spec_file_source')->value === SpecFetcherInterface::SPEC_AS_URL) { 81 | \Drupal::service('apigee_api_catalog.spec_fetcher') 82 | ->fetchSpec($entity); 83 | } 84 | 85 | // API docs that use the "file" source will still need their md5 updated. 86 | if ($entity->get('field_apidoc_spec_file_source')->value === SpecFetcherInterface::SPEC_AS_FILE) { 87 | $spec_value = $entity->get('field_apidoc_spec')->isEmpty() ? [] : $entity->get('field_apidoc_spec')->getValue()[0]; 88 | if (!empty($spec_value['target_id'])) { 89 | /* @var \Drupal\file\Entity\File $file */ 90 | $file = \Drupal::entityTypeManager() 91 | ->getStorage('file') 92 | ->load($spec_value['target_id']); 93 | 94 | if ($file) { 95 | $entity->set('field_apidoc_spec_md5', md5_file($file->getFileUri())); 96 | } 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Implements hook_entity_operation(). 103 | */ 104 | function apigee_api_catalog_entity_operation(EntityInterface $entity) { 105 | $operations = []; 106 | 107 | if ($entity->bundle() == 'apidoc' && $entity->access('update')) { 108 | if ($entity->hasLinkTemplate('reimport-spec-form')) { 109 | $operations['reimport_spec'] = [ 110 | 'title' => t('Re-import OpenAPI spec'), 111 | 'weight' => 100, 112 | 'url' => $entity->toUrl('reimport-spec-form')->setOption('query', \Drupal::destination()->getAsArray()), 113 | ]; 114 | } 115 | } 116 | 117 | return $operations; 118 | } 119 | 120 | /** 121 | * Implements hook_ENTITY_TYPE_insert(). 122 | */ 123 | function apigee_api_catalog_node_insert(EntityInterface $entity) { 124 | apigee_api_catalog_node_update($entity); 125 | } 126 | 127 | /** 128 | * Implements hook_ENTITY_TYPE_update(). 129 | */ 130 | function apigee_api_catalog_node_update(EntityInterface $entity) { 131 | if ($entity->bundle() == 'apidoc') { 132 | $systemPath = '/node/' . $entity->id(); 133 | $langCode = $entity->language()->getId(); 134 | $exists = \Drupal::service('path_alias.manager')->getAliasByPath($systemPath, $langCode); 135 | if ($exists == $systemPath) { 136 | \Drupal::entityTypeManager()->getStorage('path_alias')->create([ 137 | 'path' => $systemPath, 138 | 'alias' => '/api/' . $entity->id(), 139 | 'langcode' => $langCode, 140 | ]) 141 | ->save(); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * Implements hook_form_FORM_ID_alter(). 148 | */ 149 | function apigee_api_catalog_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { 150 | $node = $form_state->getFormObject()->getEntity(); 151 | if ($node->bundle() != 'apidoc') { 152 | return; 153 | } 154 | 155 | $form['field_apidoc_spec']['#states'] = [ 156 | 'visible' => [ 157 | ':input[name="field_apidoc_spec_file_source"]' => ['value' => SpecFetcherInterface::SPEC_AS_FILE], 158 | ], 159 | ]; 160 | 161 | // @todo: #states does not work on managed files. 162 | // @see: https://www.drupal.org/project/drupal/issues/2847425 163 | $form['field_apidoc_spec']['widget'][0]['#states'] = [ 164 | 'required' => [ 165 | ':input[name="field_apidoc_spec_file_source"]' => ['value' => SpecFetcherInterface::SPEC_AS_FILE], 166 | ], 167 | ]; 168 | 169 | $form['field_apidoc_file_link']['#states'] = [ 170 | 'visible' => [ 171 | [':input[name="field_apidoc_spec_file_source"]' => ['value' => SpecFetcherInterface::SPEC_AS_URL]], 172 | ], 173 | ]; 174 | 175 | $form['field_apidoc_file_link']['widget'][0]['uri']['#states'] = [ 176 | 'required' => [ 177 | [':input[name="field_apidoc_spec_file_source"]' => ['value' => SpecFetcherInterface::SPEC_AS_URL]], 178 | ], 179 | ]; 180 | 181 | $form['#validate'][] = '_apigee_api_catalog_form_node_form_validate'; 182 | } 183 | 184 | /** 185 | * Form validator for the "apidoc" node bundle. 186 | * 187 | * @param array $form 188 | * The form. 189 | * @param \Drupal\Core\Form\FormStateInterface $form_state 190 | * The form state. 191 | */ 192 | function _apigee_api_catalog_form_node_form_validate(&$form, FormStateInterface $form_state) { 193 | $values = $form_state->getValues(); 194 | 195 | if (empty($values['field_apidoc_spec_file_source'])) { 196 | return; 197 | } 198 | 199 | // Make sure the field_apidoc_spec (file) or field_apidoc_file_link (link) 200 | // is not empty, according to what was selected as the file source. 201 | $source = $values['field_apidoc_spec_file_source'][0]['value'] ?: NULL; 202 | if ($source == SpecFetcherInterface::SPEC_AS_FILE && empty($values['field_apidoc_spec'][0]['fids'][0])) { 203 | $form_state->setErrorByName('field_apidoc_spec', t('Provide an OpenAPI specification file.')); 204 | } 205 | elseif ($source == SpecFetcherInterface::SPEC_AS_URL && empty($values['field_apidoc_file_link'][0]['uri'])) { 206 | $form_state->setErrorByName('field_apidoc_file_link', t('Provide the URL to an OpenAPI specification file.')); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /apigee_api_catalog.routing.yml: -------------------------------------------------------------------------------- 1 | entity.node.reimport_spec_form: 2 | path: '/node/{node}/reimport' 3 | defaults: 4 | _title: 'Re-import API Doc OpenAPI specification' 5 | _entity_form: 'node.reimport_spec' 6 | requirements: 7 | _custom_access: '\Drupal\apigee_api_catalog\Entity\Form\ApiDocReimportSpecForm::checkAccess' 8 | options: 9 | _node_operation_route: TRUE 10 | -------------------------------------------------------------------------------- /apigee_api_catalog.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | logger.channel.apigee_api_catalog: 4 | parent: logger.channel_base 5 | arguments: ['apigee_api_catalog'] 6 | 7 | apigee_api_catalog.spec_fetcher: 8 | class: Drupal\apigee_api_catalog\SpecFetcher 9 | arguments: ['@file_system', '@http_client', '@entity_type.manager', '@string_translation', '@messenger', '@logger.channel.apigee_api_catalog'] 10 | 11 | apigee_api_catalog.page_not_found_subscriber: 12 | class: Drupal\apigee_api_catalog\EventSubscriber\PageNotFoundEventSubscriber 13 | arguments: ['@path.matcher', '@path.validator'] 14 | tags: 15 | - { name: event_subscriber } 16 | 17 | apigee_api_catalog.updates: 18 | class: Drupal\apigee_api_catalog\UpdateService 19 | arguments: ['@uuid', '@config.factory', '@module_handler', '@entity_type.manager', '@entity_field.manager', '@entity.last_installed_schema.repository'] 20 | 21 | apigee_api_catalog.breadcrumb: 22 | class: Drupal\apigee_api_catalog\ApigeeApiCatalogBreadcrumbBuilder 23 | tags: 24 | - { name: breadcrumb_builder, priority: 1000 } 25 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: no 10 | patch: no 11 | changes: no 12 | default_rules: 13 | flag_coverage_not_uploaded_behavior: include 14 | 15 | github_checks: 16 | annotations: yes 17 | parsers: 18 | gcov: 19 | branch_detection: 20 | conditional: yes 21 | loop: yes 22 | method: no 23 | macro: no 24 | 25 | comment: 26 | layout: "reach,diff,flags,tree" 27 | behavior: default 28 | require_changes: no 29 | show_carryforward_flags: no 30 | 31 | fixes: 32 | - "drupal/modules/contrib/apigee_api_catalog::/" 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/apigee_api_catalog", 3 | "license": "GPL-2.0", 4 | "type": "drupal-module", 5 | "description": "Apigee API Catalog for Drupal", 6 | "require": { 7 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0", 8 | "drupal/entity": "^1.6", 9 | "drupal/file_link": "^2.2", 10 | "drupal/apigee_edge": "^3.0.8 || ^4.0.0", 11 | "webonyx/graphql-php": "^15.19" 12 | }, 13 | "require-dev": { 14 | "cweagans/composer-patches": "^1.7", 15 | "drupal/core-dev": "^10.3 || ^11.1", 16 | "drush/drush": "^12.4.3 || ^13.3", 17 | "mglaman/drupal-check": "^1.5", 18 | "phpmd/phpmd": "^2.15.0", 19 | "phpmetrics/phpmetrics": "^2.8", 20 | "phpstan/phpstan": "^1.12" 21 | }, 22 | "config": { 23 | "sort-packages": true 24 | }, 25 | "minimum-stability": "dev", 26 | "prefer-stable": true 27 | } 28 | -------------------------------------------------------------------------------- /config/install/core.base_field_override.node.apidoc.title.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - node.type.apidoc 6 | id: node.apidoc.title 7 | field_name: title 8 | entity_type: node 9 | bundle: apidoc 10 | label: Name 11 | description: '' 12 | required: true 13 | translatable: true 14 | default_value: { } 15 | default_value_callback: '' 16 | settings: { } 17 | field_type: string 18 | -------------------------------------------------------------------------------- /config/install/core.entity_form_display.node.apidoc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.apidoc.body 6 | - field.field.node.apidoc.field_api_product 7 | - field.field.node.apidoc.field_apidoc_fetched_timestamp 8 | - field.field.node.apidoc.field_apidoc_file_link 9 | - field.field.node.apidoc.field_apidoc_spec 10 | - field.field.node.apidoc.field_apidoc_spec_file_source 11 | - field.field.node.apidoc.field_apidoc_spec_md5 12 | - node.type.apidoc 13 | module: 14 | - file 15 | - file_link 16 | - path 17 | - text 18 | id: node.apidoc.default 19 | targetEntityType: node 20 | bundle: apidoc 21 | mode: default 22 | content: 23 | body: 24 | type: text_textarea_with_summary 25 | weight: 1 26 | region: content 27 | settings: 28 | rows: 9 29 | summary_rows: 3 30 | placeholder: '' 31 | show_summary: false 32 | third_party_settings: { } 33 | created: 34 | type: datetime_timestamp 35 | weight: 6 36 | region: content 37 | settings: { } 38 | third_party_settings: { } 39 | field_apidoc_file_link: 40 | type: file_link_default 41 | weight: 4 42 | region: content 43 | settings: { } 44 | third_party_settings: { } 45 | field_apidoc_spec: 46 | weight: 3 47 | settings: 48 | progress_indicator: throbber 49 | third_party_settings: { } 50 | type: file_generic 51 | region: content 52 | field_apidoc_spec_file_source: 53 | weight: 2 54 | settings: { } 55 | third_party_settings: { } 56 | type: options_select 57 | region: content 58 | field_api_product: 59 | weight: 7 60 | settings: 61 | match_operator: CONTAINS 62 | match_limit: 10 63 | size: 60 64 | placeholder: '' 65 | third_party_settings: { } 66 | type: entity_reference_autocomplete 67 | region: content 68 | path: 69 | type: path 70 | weight: 9 71 | region: content 72 | settings: { } 73 | third_party_settings: { } 74 | promote: 75 | type: boolean_checkbox 76 | settings: 77 | display_label: true 78 | weight: 7 79 | region: content 80 | third_party_settings: { } 81 | status: 82 | type: boolean_checkbox 83 | settings: 84 | display_label: true 85 | weight: 10 86 | region: content 87 | third_party_settings: { } 88 | sticky: 89 | type: boolean_checkbox 90 | settings: 91 | display_label: true 92 | weight: 8 93 | region: content 94 | third_party_settings: { } 95 | title: 96 | type: string_textfield 97 | weight: 0 98 | region: content 99 | settings: 100 | size: 60 101 | placeholder: '' 102 | third_party_settings: { } 103 | uid: 104 | type: entity_reference_autocomplete 105 | weight: 5 106 | settings: 107 | match_operator: CONTAINS 108 | size: 60 109 | placeholder: '' 110 | match_limit: 10 111 | region: content 112 | third_party_settings: { } 113 | hidden: 114 | field_apidoc_fetched_timestamp: true 115 | field_apidoc_spec_md5: true 116 | -------------------------------------------------------------------------------- /config/install/core.entity_view_display.node.apidoc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.apidoc.body 6 | - field.field.node.apidoc.field_api_product 7 | - field.field.node.apidoc.field_apidoc_fetched_timestamp 8 | - field.field.node.apidoc.field_apidoc_file_link 9 | - field.field.node.apidoc.field_apidoc_spec 10 | - field.field.node.apidoc.field_apidoc_spec_file_source 11 | - field.field.node.apidoc.field_apidoc_spec_md5 12 | - node.type.apidoc 13 | module: 14 | - apigee_api_catalog 15 | - text 16 | - user 17 | id: node.apidoc.default 18 | targetEntityType: node 19 | bundle: apidoc 20 | mode: default 21 | content: 22 | body: 23 | type: text_default 24 | weight: 0 25 | region: content 26 | label: hidden 27 | settings: { } 28 | third_party_settings: { } 29 | field_apidoc_spec: 30 | weight: 1 31 | label: hidden 32 | settings: { } 33 | third_party_settings: { } 34 | type: apigee_api_catalog_smartdocs 35 | region: content 36 | links: 37 | weight: 2 38 | region: content 39 | settings: { } 40 | third_party_settings: { } 41 | hidden: 42 | field_apidoc_fetched_timestamp: true 43 | field_apidoc_file_link: true 44 | field_apidoc_spec_file_source: true 45 | field_apidoc_spec_md5: true 46 | field_api_product: true 47 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.body.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.body 6 | - node.type.apidoc 7 | module: 8 | - text 9 | id: node.apidoc.body 10 | field_name: body 11 | entity_type: node 12 | bundle: apidoc 13 | label: Description 14 | description: 'Description of the API.' 15 | required: false 16 | translatable: true 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | display_summary: true 21 | required_summary: false 22 | field_type: text_with_summary 23 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_api_product.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_api_product 6 | - node.type.apidoc 7 | id: node.apidoc.field_api_product 8 | field_name: field_api_product 9 | entity_type: node 10 | bundle: apidoc 11 | label: 'API Product' 12 | description: 'Apigee API Product' 13 | required: false 14 | translatable: false 15 | default_value: { } 16 | default_value_callback: '' 17 | settings: 18 | handler: 'default:api_product' 19 | handler_settings: 20 | target_bundles: null 21 | auto_create: false 22 | field_type: entity_reference 23 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_apidoc_fetched_timestamp.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_apidoc_fetched_timestamp 6 | - node.type.apidoc 7 | id: node.apidoc.field_apidoc_fetched_timestamp 8 | field_name: field_apidoc_fetched_timestamp 9 | entity_type: node 10 | bundle: apidoc 11 | label: 'Spec fetched from URL timestamp' 12 | description: 'When the OpenAPI spec file was last fetched from URL as a Unix timestamp.' 13 | required: false 14 | translatable: false 15 | default_value: 16 | - 17 | value: 1587191891 18 | default_value_callback: '' 19 | settings: { } 20 | field_type: timestamp 21 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_apidoc_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_apidoc_file_link 6 | - node.type.apidoc 7 | module: 8 | - file_link 9 | id: node.apidoc.field_apidoc_file_link 10 | field_name: field_apidoc_file_link 11 | entity_type: node 12 | bundle: apidoc 13 | label: 'URL to OpenAPI specification file' 14 | description: 'The URL to an OpenAPI file spec.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | link_type: 17 21 | title: 0 22 | file_extensions: 'yaml json' 23 | no_extension: false 24 | field_type: file_link 25 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_apidoc_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_apidoc_spec 6 | - node.type.apidoc 7 | module: 8 | - file 9 | id: node.apidoc.field_apidoc_spec 10 | field_name: field_apidoc_spec 11 | entity_type: node 12 | bundle: apidoc 13 | label: 'OpenAPI specification' 14 | description: 'The spec snapshot.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | file_directory: apidoc_specs 21 | file_extensions: 'yaml json' 22 | max_filesize: '' 23 | description_field: false 24 | handler: 'default:file' 25 | handler_settings: { } 26 | field_type: file 27 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_apidoc_spec_file_source.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_apidoc_spec_file_source 6 | - node.type.apidoc 7 | module: 8 | - options 9 | id: node.apidoc.field_apidoc_spec_file_source 10 | field_name: field_apidoc_spec_file_source 11 | entity_type: node 12 | bundle: apidoc 13 | label: 'Specification source type' 14 | description: 'Indicate if the OpenAPI spec will be provided as a file for upload or a URL.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: { } 20 | field_type: list_string 21 | -------------------------------------------------------------------------------- /config/install/field.field.node.apidoc.field_apidoc_spec_md5.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_apidoc_spec_md5 6 | - node.type.apidoc 7 | id: node.apidoc.field_apidoc_spec_md5 8 | field_name: field_apidoc_spec_md5 9 | entity_type: node 10 | bundle: apidoc 11 | label: 'OpenAPI specification file MD5' 12 | description: 'OpenAPI specification file MD5.' 13 | required: false 14 | translatable: false 15 | default_value: { } 16 | default_value_callback: '' 17 | settings: { } 18 | field_type: string 19 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_api_product.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - apigee_edge 9 | - node 10 | id: node.field_api_product 11 | field_name: field_api_product 12 | entity_type: node 13 | type: entity_reference 14 | settings: 15 | target_type: api_product 16 | module: apigee_api_catalog 17 | locked: false 18 | cardinality: -1 19 | translatable: false 20 | indexes: { } 21 | persist_with_no_fields: false 22 | custom_storage: false 23 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_apidoc_fetched_timestamp.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - node 9 | id: node.field_apidoc_fetched_timestamp 10 | field_name: field_apidoc_fetched_timestamp 11 | entity_type: node 12 | type: timestamp 13 | settings: { } 14 | module: core 15 | locked: false 16 | cardinality: 1 17 | translatable: true 18 | indexes: { } 19 | persist_with_no_fields: false 20 | custom_storage: false 21 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_apidoc_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - file_link 9 | - node 10 | id: node.field_apidoc_file_link 11 | field_name: field_apidoc_file_link 12 | entity_type: node 13 | type: file_link 14 | settings: { } 15 | module: file_link 16 | locked: false 17 | cardinality: 1 18 | translatable: true 19 | indexes: { } 20 | persist_with_no_fields: false 21 | custom_storage: false 22 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_apidoc_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - file 9 | - node 10 | id: node.field_apidoc_spec 11 | field_name: field_apidoc_spec 12 | entity_type: node 13 | type: file 14 | settings: 15 | display_field: false 16 | display_default: false 17 | uri_scheme: public 18 | target_type: file 19 | module: file 20 | locked: false 21 | cardinality: 1 22 | translatable: true 23 | indexes: { } 24 | persist_with_no_fields: false 25 | custom_storage: false 26 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_apidoc_spec_file_source.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - node 9 | - options 10 | id: node.field_apidoc_spec_file_source 11 | field_name: field_apidoc_spec_file_source 12 | entity_type: node 13 | type: list_string 14 | settings: 15 | allowed_values: 16 | - 17 | value: file 18 | label: File 19 | - 20 | value: url 21 | label: URL 22 | allowed_values_function: '' 23 | module: options 24 | locked: false 25 | cardinality: 1 26 | translatable: true 27 | indexes: { } 28 | persist_with_no_fields: false 29 | custom_storage: false 30 | -------------------------------------------------------------------------------- /config/install/field.storage.node.field_apidoc_spec_md5.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | module: 8 | - node 9 | id: node.field_apidoc_spec_md5 10 | field_name: field_apidoc_spec_md5 11 | entity_type: node 12 | type: string 13 | settings: 14 | max_length: 255 15 | is_ascii: false 16 | case_sensitive: false 17 | module: core 18 | locked: false 19 | cardinality: 1 20 | translatable: true 21 | indexes: { } 22 | persist_with_no_fields: false 23 | custom_storage: false 24 | -------------------------------------------------------------------------------- /config/install/node.type.apidoc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_api_catalog 7 | name: 'OpenAPI Doc' 8 | type: apidoc 9 | description: 'Use OpenAPI Docs to document OpenAPIs' 10 | help: '' 11 | new_revision: true 12 | preview_mode: 1 13 | display_submitted: false 14 | -------------------------------------------------------------------------------- /js/smartdocs_integration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Integration of SmartDocs file field formatter. 4 | * 5 | * Take the OpenAPI spec and put into session storage for the SmartDocs 6 | * Angular app to use. 7 | */ 8 | 9 | (function ($, window, Drupal) { 10 | 11 | Drupal.behaviors.smartdocsFieldFormatter = { 12 | attach: function (context) { 13 | let specMap = {}; 14 | for (let fieldName in drupalSettings.smartdocsFieldFormatter) { 15 | if (drupalSettings.smartdocsFieldFormatter.hasOwnProperty(fieldName)) { 16 | const field = drupalSettings.smartdocsFieldFormatter[fieldName]; 17 | for (let fieldDelta = 0; fieldDelta < field.openApiFiles.length; fieldDelta++) { 18 | // Each openApiFile var has the spec url and file extension. 19 | const fileUrl = field.openApiFiles[fieldDelta].fileUrl; 20 | const fileExtention = field.openApiFiles[fieldDelta].fileExtension; 21 | // Get the OpenAPI spec file from the server. 22 | $.get(fileUrl, function(data, status) { 23 | if (fileExtention === 'json') { 24 | // JSON files do not need any conversion. 25 | specMap[field.entityId] = data; 26 | } 27 | else { 28 | // YAML files need to be parsed into javascript object. 29 | specMap[field.entityId] = jsyaml.load(data); 30 | } 31 | // Store the spec into session storage. 32 | sessionStorage.setItem('specs', JSON.stringify(specMap)); 33 | }); 34 | } 35 | } 36 | } 37 | } 38 | }; 39 | 40 | }(jQuery, window, Drupal)); 41 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.info.yml: -------------------------------------------------------------------------------- 1 | name: AsyncAPI for Apigee 2 | type: module 3 | description: AsyncAPI for Apigee 4 | package: Apigee (Experimental) 5 | core_version_requirement: ^10 || ^11 6 | dependencies: 7 | - apigee_api_catalog 8 | - file_link 9 | - text 10 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.install: -------------------------------------------------------------------------------- 1 | get('node.type.locked'); 32 | $locked['asyncapi_doc'] = 'asyncapi_doc'; 33 | \Drupal::state()->set('node.type.locked', $locked); 34 | } 35 | 36 | /** 37 | * Implements hook_uninstall(). 38 | */ 39 | function apigee_asyncapi_doc_uninstall() { 40 | // Allow to delete a asyncapi_doc's node type. 41 | $locked = \Drupal::state()->get('node.type.locked'); 42 | unset($locked['asyncapi_doc']); 43 | \Drupal::state()->set('node.type.locked', $locked); 44 | } 45 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.libraries.yml: -------------------------------------------------------------------------------- 1 | # Third-party library (CDN). 2 | reactjs: 3 | remote: https://reactjs.org 4 | version: VERSION 5 | license: 6 | name: MIT 7 | url: https://github.com/facebook/react/blob/main/LICENSE 8 | gpl-compatible: true 9 | js: 10 | https://unpkg.com/react/umd/react.production.min.js: {type: external, minified: true} 11 | https://unpkg.com/react-dom/umd/react-dom.production.min.js: {type: external, minified: true} 12 | 13 | # Third-party library (CDN). 14 | # When updating the version, apigee-async-doc.css should be updated as well. 15 | asyncapi: 16 | remote: https://www.asyncapi.com/ 17 | version: VERSION 18 | license: 19 | name: Apache License 2.0 20 | url: https://github.com/asyncapi/asyncapi-react/blob/master/LICENSE 21 | gpl-compatible: true 22 | js: 23 | https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-bundle.js: {type: external, minified: true} 24 | https://unpkg.com/@asyncapi/web-component/lib/asyncapi-web-component.js: {type: external, minified: true} 25 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.links.action.yml: -------------------------------------------------------------------------------- 1 | node.asyncapi_doc.add_form: 2 | route_name: node.add 3 | title: 'AsyncAPI' 4 | route_parameters: 5 | node_type: 'asyncapi_doc' 6 | appears_on: 7 | - view.api_catalog_admin.page_1 8 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.module: -------------------------------------------------------------------------------- 1 | [ 35 | 'variables' => [ 36 | 'field_name' => NULL, 37 | 'delta' => NULL, 38 | 'link' => NULL, 39 | ], 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * Implements hook_ENTITY_TYPE_presave(). 46 | */ 47 | function apigee_asyncapi_doc_node_presave(EntityInterface $entity) { 48 | /* @var \Drupal\node\NodeInterface $entity */ 49 | 50 | if ($entity->bundle() != 'asyncapi_doc') { 51 | return; 52 | } 53 | 54 | if ($entity->get('field_asyncapi_spec_source_type')->value === 'file') { 55 | $spec_value = $entity->get('field_asyncapi_spec')->isEmpty() ? [] : $entity->get('field_asyncapi_spec')->getValue()[0]; 56 | if (!empty($spec_value['target_id'])) { 57 | /* @var \Drupal\file\Entity\File $file */ 58 | $file = \Drupal::entityTypeManager() 59 | ->getStorage('file') 60 | ->load($spec_value['target_id']); 61 | 62 | if ($file) { 63 | $entity->set('field_asyncapi_spec_file_link', ['uri' => $file->createFileUrl(FALSE)]); 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Implements hook_form_FORM_ID_alter(). 71 | */ 72 | function apigee_asyncapi_doc_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { 73 | $node = $form_state->getFormObject()->getEntity(); 74 | if ($node->bundle() != 'asyncapi_doc') { 75 | return; 76 | } 77 | 78 | $form['field_asyncapi_spec']['#states'] = [ 79 | 'visible' => [ 80 | ':input[name="field_asyncapi_spec_source_type"]' => ['value' => 'file'], 81 | ], 82 | ]; 83 | 84 | // @todo: #states does not work on managed files. 85 | // @see: https://www.drupal.org/project/drupal/issues/2847425 86 | $form['field_asyncapi_spec']['widget'][0]['#states'] = [ 87 | 'required' => [ 88 | ':input[name="field_asyncapi_spec_source_type"]' => ['value' => 'file'], 89 | ], 90 | ]; 91 | 92 | $form['field_asyncapi_spec_file_link']['#states'] = [ 93 | 'visible' => [ 94 | [':input[name="field_asyncapi_spec_source_type"]' => ['value' => 'url']], 95 | ], 96 | ]; 97 | 98 | $form['field_asyncapi_spec_file_link']['widget'][0]['uri']['#states'] = [ 99 | 'required' => [ 100 | [':input[name="field_asyncapi_spec_source_type"]' => ['value' => 'url']], 101 | ], 102 | ]; 103 | 104 | $form['#validate'][] = '_apigee_asyncapi_form_node_form_validate'; 105 | } 106 | 107 | /** 108 | * Form validator for the "asyncapi_doc" node bundle. 109 | * 110 | * @param array $form 111 | * The form. 112 | * @param \Drupal\Core\Form\FormStateInterface $form_state 113 | * The form state. 114 | */ 115 | function _apigee_asyncapi_form_node_form_validate(&$form, FormStateInterface $form_state) { 116 | $values = $form_state->getValues(); 117 | 118 | if (empty($values['field_asyncapi_spec_source_type'])) { 119 | return; 120 | } 121 | 122 | // Make sure the field_asyncapi_spec (file) or field_asyncapi_spec_file_link (link) 123 | // is not empty, according to what was selected as the file source. 124 | $source = $values['field_asyncapi_spec_source_type'][0]['value'] ?: NULL; 125 | if ($source == 'file' && empty($values['field_asyncapi_spec'][0]['fids'][0])) { 126 | $form_state->setErrorByName('field_asyncapi_spec', t('Provide an AsyncAPI specification file.')); 127 | } 128 | elseif ($source == 'url' && empty($values['field_asyncapi_spec_file_link'][0]['uri'])) { 129 | $form_state->setErrorByName('field_asyncapi_spec_file_link', t('Provide the URL to an AsyncAPI specification file.')); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.routing.yml: -------------------------------------------------------------------------------- 1 | apigee_asyncapi_doc.confirm_remove_views_references: 2 | path: '/admin/modules/uninstall/apigee-async-doc/confirm-remove-views-references' 3 | defaults: 4 | _title: 'Confirm remove views references' 5 | _form: 'Drupal\apigee_asyncapi_doc\Form\ConfirmRemoveViewsReferencesForm' 6 | requirements: 7 | _permission: 'administer modules' 8 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/apigee_asyncapi_doc.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | apigee_asyncapi_doc.event_subscriber: 3 | class: Drupal\apigee_asyncapi_doc\EventSubscriber\ApigeeAsyncapiDocSubscriber 4 | arguments: ['@messenger'] 5 | tags: 6 | - { name: event_subscriber } 7 | 8 | apigee_asyncapi_doc.uninstall_validator: 9 | class: Drupal\apigee_asyncapi_doc\ApigeeAsyncapiDocUninstallValidator 10 | tags: 11 | - { name: module_install.uninstall_validator } 12 | arguments: ['@entity_type.manager', '@string_translation'] 13 | 14 | apigee_asyncapi_doc.breadcrumb: 15 | class: Drupal\apigee_asyncapi_doc\ApigeeAsyncapiDocBreadcrumbBuilder 16 | tags: 17 | - { name: breadcrumb_builder, priority: 1000 } 18 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.field.node.asyncapi_doc.body.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.body 6 | - node.type.asyncapi_doc 7 | module: 8 | - text 9 | id: node.asyncapi_doc.body 10 | field_name: body 11 | entity_type: node 12 | bundle: asyncapi_doc 13 | label: Description 14 | description: '' 15 | required: false 16 | translatable: true 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | display_summary: true 21 | required_summary: false 22 | field_type: text_with_summary 23 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.field.node.asyncapi_doc.field_api_product.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_api_product 6 | - node.type.asyncapi_doc 7 | id: node.asyncapi_doc.field_api_product 8 | field_name: field_api_product 9 | entity_type: node 10 | bundle: asyncapi_doc 11 | label: 'API Product' 12 | description: 'Apigee API Product' 13 | required: false 14 | translatable: false 15 | default_value: { } 16 | default_value_callback: '' 17 | settings: 18 | handler: 'default:api_product' 19 | handler_settings: 20 | target_bundles: null 21 | auto_create: false 22 | field_type: entity_reference 23 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.field.node.asyncapi_doc.field_asyncapi_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_asyncapi_spec 6 | - node.type.asyncapi_doc 7 | module: 8 | - file 9 | id: node.asyncapi_doc.field_asyncapi_spec 10 | field_name: field_asyncapi_spec 11 | entity_type: node 12 | bundle: asyncapi_doc 13 | label: 'AsyncAPI specification' 14 | description: 'The spec snapshot.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | handler: 'default:file' 21 | handler_settings: { } 22 | file_directory: asyncapi_specs 23 | file_extensions: 'yaml json' 24 | max_filesize: '' 25 | description_field: false 26 | field_type: file -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.field.node.asyncapi_doc.field_asyncapi_spec_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_asyncapi_spec_file_link 6 | - node.type.asyncapi_doc 7 | module: 8 | - file_link 9 | id: node.asyncapi_doc.field_asyncapi_spec_file_link 10 | field_name: field_asyncapi_spec_file_link 11 | entity_type: node 12 | bundle: asyncapi_doc 13 | label: 'AsyncAPI endpoint' 14 | description: '' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | title: 0 21 | link_type: 16 22 | file_extensions: '' 23 | no_extension: true 24 | deferred_request: false 25 | field_type: file_link 26 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.field.node.asyncapi_doc.field_asyncapi_spec_source_type.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_asyncapi_spec_source_type 6 | - node.type.asyncapi_doc 7 | module: 8 | - options 9 | id: node.asyncapi_doc.field_asyncapi_spec_source_type 10 | field_name: field_asyncapi_spec_source_type 11 | entity_type: node 12 | bundle: asyncapi_doc 13 | label: 'Specification source type' 14 | description: 'Indicate if the AsyncAPI spec will be provided as a file for upload or a URL.' 15 | required: true 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: { } 20 | field_type: list_string 21 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.storage.node.field_asyncapi_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_asyncapi_doc 7 | module: 8 | - file 9 | - node 10 | id: node.field_asyncapi_spec 11 | field_name: field_asyncapi_spec 12 | entity_type: node 13 | type: file 14 | settings: 15 | target_type: file 16 | display_field: false 17 | display_default: false 18 | uri_scheme: public 19 | module: file 20 | locked: false 21 | cardinality: 1 22 | translatable: false 23 | indexes: { } 24 | persist_with_no_fields: false 25 | custom_storage: false 26 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.storage.node.field_asyncapi_spec_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_asyncapi_doc 7 | module: 8 | - file_link 9 | - node 10 | id: node.field_asyncapi_spec_file_link 11 | field_name: field_asyncapi_spec_file_link 12 | entity_type: node 13 | type: file_link 14 | settings: { } 15 | module: file_link 16 | locked: false 17 | cardinality: 1 18 | translatable: false 19 | indexes: { } 20 | persist_with_no_fields: false 21 | custom_storage: false 22 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/field.storage.node.field_asyncapi_spec_source_type.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_asyncapi_doc 7 | module: 8 | - node 9 | - options 10 | id: node.field_asyncapi_spec_source_type 11 | field_name: field_asyncapi_spec_source_type 12 | entity_type: node 13 | type: list_string 14 | settings: 15 | allowed_values: 16 | - 17 | value: file 18 | label: File 19 | - 20 | value: url 21 | label: URL 22 | allowed_values_function: '' 23 | module: options 24 | locked: false 25 | cardinality: 1 26 | translatable: false 27 | indexes: { } 28 | persist_with_no_fields: false 29 | custom_storage: false -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/install/node.type.asyncapi_doc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | dependencies: 3 | module: 4 | - apigee_api_catalog 5 | - apigee_asyncapi_doc 6 | enforced: 7 | module: 8 | - apigee_asyncapi_doc 9 | type: asyncapi_doc 10 | name: 'AsyncAPI Doc' 11 | description: 'Use AsyncAPI Doc to document AsyncAPI.' 12 | help: '' 13 | new_revision: false 14 | display_submitted: false 15 | preview_mode: 0 16 | status: true 17 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/optional/core.entity_form_display.node.asyncapi_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.asyncapi_doc.body 6 | - field.field.node.asyncapi_doc.field_asyncapi_spec 7 | - field.field.node.asyncapi_doc.field_asyncapi_spec_file_link 8 | - field.field.node.asyncapi_doc.field_asyncapi_spec_source_type 9 | - field.field.node.asyncapi_doc.field_api_product 10 | - node.type.asyncapi_doc 11 | module: 12 | - file_link 13 | - path 14 | - text 15 | id: node.asyncapi_doc.default 16 | targetEntityType: node 17 | bundle: asyncapi_doc 18 | mode: default 19 | content: 20 | title: 21 | type: string_textfield 22 | weight: 0 23 | region: content 24 | settings: 25 | size: 60 26 | placeholder: '' 27 | third_party_settings: { } 28 | body: 29 | type: text_textarea_with_summary 30 | weight: 1 31 | settings: 32 | rows: 9 33 | summary_rows: 3 34 | placeholder: '' 35 | show_summary: false 36 | third_party_settings: { } 37 | region: content 38 | field_asyncapi_spec_source_type: 39 | type: options_select 40 | weight: 4 41 | region: content 42 | settings: { } 43 | third_party_settings: { } 44 | field_asyncapi_spec: 45 | type: file_generic 46 | weight: 5 47 | region: content 48 | settings: 49 | progress_indicator: throbber 50 | third_party_settings: { } 51 | field_asyncapi_spec_file_link: 52 | type: file_link_default 53 | weight: 6 54 | region: content 55 | settings: 56 | placeholder_url: '' 57 | placeholder_title: '' 58 | third_party_settings: { } 59 | field_api_product: 60 | weight: 7 61 | settings: 62 | match_operator: CONTAINS 63 | match_limit: 10 64 | size: 60 65 | placeholder: '' 66 | third_party_settings: { } 67 | type: entity_reference_autocomplete 68 | region: content 69 | created: 70 | type: datetime_timestamp 71 | weight: 8 72 | region: content 73 | settings: { } 74 | third_party_settings: { } 75 | promote: 76 | type: boolean_checkbox 77 | settings: 78 | display_label: true 79 | weight: 9 80 | region: content 81 | third_party_settings: { } 82 | uid: 83 | type: entity_reference_autocomplete 84 | weight: 10 85 | settings: 86 | match_operator: CONTAINS 87 | size: 60 88 | placeholder: '' 89 | match_limit: 10 90 | region: content 91 | third_party_settings: { } 92 | sticky: 93 | type: boolean_checkbox 94 | settings: 95 | display_label: true 96 | weight: 11 97 | region: content 98 | third_party_settings: { } 99 | path: 100 | type: path 101 | weight: 12 102 | region: content 103 | settings: { } 104 | third_party_settings: { } 105 | status: 106 | type: boolean_checkbox 107 | settings: 108 | display_label: true 109 | weight: 13 110 | region: content 111 | third_party_settings: { } 112 | hidden: { } -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/config/optional/core.entity_view_display.node.asyncapi_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.asyncapi_doc.body 6 | - field.field.node.asyncapi_doc.field_asyncapi_spec 7 | - field.field.node.asyncapi_doc.field_asyncapi_spec_file_link 8 | - field.field.node.asyncapi_doc.field_asyncapi_spec_source_type 9 | - field.field.node.asyncapi_doc.field_api_product 10 | - node.type.asyncapi_doc 11 | module: 12 | - file_link 13 | - text 14 | - user 15 | id: node.asyncapi_doc.default 16 | targetEntityType: node 17 | bundle: asyncapi_doc 18 | mode: default 19 | content: 20 | body: 21 | label: hidden 22 | type: text_default 23 | weight: 1 24 | settings: { } 25 | third_party_settings: { } 26 | region: content 27 | field_asyncapi_spec_file_link: 28 | weight: 3 29 | label: hidden 30 | settings: 31 | trim_length: 80 32 | url_only: true 33 | url_plain: false 34 | rel: '' 35 | target: '' 36 | format_size: true 37 | third_party_settings: { } 38 | type: apigee_asyncapi_doc_async 39 | region: content 40 | links: 41 | weight: 0 42 | region: content 43 | settings: { } 44 | third_party_settings: { } 45 | hidden: 46 | field_api_product: true 47 | field_asyncapi_spec: true 48 | field_asyncapi_spec_source_type: true 49 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/css/apigee-asyncapi-doc.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License version 2 as published by the 6 | * Free Software Foundation. 7 | * 8 | * This program is distributed in the hope that it will be useful, but WITHOUT 9 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 10 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 11 | * License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License along 14 | * with this program; if not, write to the Free Software Foundation, Inc., 51 15 | * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | */ 17 | 18 | @import url(https://unpkg.com/@asyncapi/react-component/styles/default.min.css); 19 | 20 | /* Customization as per Apigee */ 21 | .asyncapi, 22 | .asyncapi__tag, 23 | .asyncapi__server-expand-icon:before, 24 | .asyncapi__error { 25 | font-family: Roboto,sans-serif; 26 | } 27 | .asyncapi__code-body { 28 | font-family: courier; 29 | } 30 | h1, h2, h3 { 31 | font-weight: 300; 32 | } 33 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/src/ApigeeAsyncapiDocBreadcrumbBuilder.php: -------------------------------------------------------------------------------- 1 | getParameter('node'); 42 | return $node instanceof NodeInterface && $node->getType() === 'asyncapi_doc'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function build(RouteMatchInterface $route_match) { 49 | $breadcrumb = new Breadcrumb(); 50 | 51 | $links[] = Link::createFromRoute($this->t('Home'), ''); 52 | 53 | // API Catalog page is a view. 54 | $links[] = Link::createFromRoute($this->t('API Catalog'), 'view.apigee_api_catalog.page_1'); 55 | 56 | $breadcrumb->setLinks($links); 57 | $breadcrumb->addCacheContexts(['route']); 58 | 59 | return $breadcrumb; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/src/ApigeeAsyncapiDocUninstallValidator.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_manager; 54 | $this->stringTranslation = $string_translation; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function validate($module) { 61 | $reasons = []; 62 | if ($module == 'apigee_asyncapi_doc') { 63 | if ($this->hasAsyncApiDocNodes()) { 64 | $reasons[] = $this->t('To uninstall AsyncAPI for Apigee, first delete all AsyncAPI Doc content'); 65 | } 66 | 67 | // Restrict uninstall, if reference of asyncapi_doc is there in apigee_api_catalog view. 68 | $is_views_references = FALSE; 69 | $view = Views::getView('apigee_api_catalog'); 70 | if (is_object($view)) { 71 | $display = $view->getDisplay(); 72 | 73 | $filters = $view->display_handler->getOption('filters'); 74 | if (isset($filters['type']['value']['asyncapi_doc'])) { 75 | $is_views_references = TRUE; 76 | } 77 | } 78 | 79 | // Restrict uninstall, if reference of asyncapi_doc is there in api_catalog_admin view. 80 | $view = Views::getView('api_catalog_admin'); 81 | if (is_object($view)) { 82 | $display = $view->getDisplay(); 83 | 84 | $filters = $view->display_handler->getOption('filters'); 85 | if (isset($filters['type']['value']['asyncapi_doc'])) { 86 | $is_views_references = TRUE; 87 | } 88 | } 89 | if ($is_views_references) { 90 | $message_arguments = [ 91 | ':url' => Url::fromRoute('apigee_asyncapi_doc.confirm_remove_views_references')->toString(), 92 | ]; 93 | $reasons[] = $this->t('Click to remove AsyncAPI Doc references from both "apigee_api_catalog" and "api_catalog_admin" views', $message_arguments); 94 | } 95 | } 96 | 97 | return $reasons; 98 | } 99 | 100 | /** 101 | * Determines if there are any asyncapi_doc nodes or not. 102 | * 103 | * @return bool 104 | * TRUE if there are forum nodes, FALSE otherwise. 105 | */ 106 | protected function hasAsyncApiDocNodes() { 107 | $nodes = $this->entityTypeManager->getStorage('node')->getQuery() 108 | ->condition('type', 'asyncapi_doc') 109 | ->accessCheck(FALSE) 110 | ->range(0, 1) 111 | ->execute(); 112 | return !empty($nodes); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/src/EventSubscriber/ApigeeAsyncapiDocSubscriber.php: -------------------------------------------------------------------------------- 1 | messenger = $messenger; 49 | } 50 | 51 | /** 52 | * Update views on installation. 53 | * 54 | * @param \Drupal\Core\Config\ConfigCrudEvent $event 55 | * Config crud event. 56 | */ 57 | public function configSave(ConfigCrudEvent $event) { 58 | $config = $event->getConfig(); 59 | if ($config->getName() == 'core.entity_view_display.node.asyncapi_doc.default') { 60 | 61 | $view = Views::getView('apigee_api_catalog'); 62 | if (is_object($view)) { 63 | $display = $view->getDisplay(); 64 | 65 | $filters = $view->display_handler->getOption('filters'); 66 | if ($filters['type']) { 67 | $filters['type']['value']['asyncapi_doc'] = 'asyncapi_doc'; 68 | } 69 | if (isset($filters['type_1'])) { 70 | $filters['type_1']['value']['asyncapi_doc'] = 'asyncapi_doc'; 71 | } 72 | $view->display_handler->overrideOption('filters', $filters); 73 | 74 | $view->save(); 75 | \Drupal::messenger()->addStatus('Updating Views - API Catalog view'); 76 | } 77 | 78 | $view = Views::getView('api_catalog_admin'); 79 | if (is_object($view)) { 80 | $display = $view->getDisplay(); 81 | 82 | $filters = $view->display_handler->getOption('filters'); 83 | if ($filters['type']) { 84 | $filters['type']['value']['asyncapi_doc'] = 'asyncapi_doc'; 85 | } 86 | if (isset($filters['type_1'])) { 87 | $filters['type_1']['value']['asyncapi_doc'] = 'asyncapi_doc'; 88 | } 89 | $view->display_handler->overrideOption('filters', $filters); 90 | 91 | $view->save(); 92 | \Drupal::messenger()->addStatus('Updating Views - API Catalog Admin'); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public static function getSubscribedEvents() { 101 | return [ 102 | ConfigEvents::SAVE => 'configSave', 103 | ]; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/src/Form/ConfirmRemoveViewsReferencesForm.php: -------------------------------------------------------------------------------- 1 | t('Are you sure you want to remove references of AsyncAPI for Apigee in views before uninstallation of the module?'); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getCancelUrl() { 51 | return new Url('system.modules_uninstall'); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function submitForm(array &$form, FormStateInterface $form_state) { 58 | $view = Views::getView('apigee_api_catalog'); 59 | if (is_object($view)) { 60 | $display = $view->getDisplay(); 61 | 62 | $filters = $view->display_handler->getOption('filters'); 63 | if ($filters['type']['value']['asyncapi_doc']) { 64 | unset($filters['type']['value']['asyncapi_doc']); 65 | } 66 | if (isset($filters['type_1']) && $filters['type_1']['value']['asyncapi_doc']) { 67 | unset($filters['type_1']['value']['asyncapi_doc']); 68 | } 69 | $view->display_handler->overrideOption('filters', $filters); 70 | 71 | $view->save(); 72 | 73 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog')); 74 | } 75 | 76 | $view = Views::getView('api_catalog_admin'); 77 | if (is_object($view)) { 78 | $display = $view->getDisplay(); 79 | 80 | $filters = $view->display_handler->getOption('filters'); 81 | if ($filters['type']['value']['asyncapi_doc']) { 82 | unset($filters['type']['value']['asyncapi_doc']); 83 | } 84 | if (isset($filters['type_1']) && $filters['type_1']['value']['asyncapi_doc']) { 85 | unset($filters['type_1']['value']['asyncapi_doc']); 86 | } 87 | $view->display_handler->overrideOption('filters', $filters); 88 | 89 | $view->save(); 90 | 91 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog Admin')); 92 | } 93 | 94 | $form_state->setRedirectUrl($this->getCancelUrl()); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/src/Plugin/Field/FieldFormatter/AsyncapiFormatter.php: -------------------------------------------------------------------------------- 1 | $item) { 46 | $element[$delta] = [ 47 | '#theme' => 'apigee_asyncapi_doc_file_link_field_item', 48 | '#field_name' => $this->fieldDefinition->getName(), 49 | '#delta' => $delta, 50 | '#link' => $item->getUrl()->toString(), 51 | ]; 52 | } 53 | 54 | return $element; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function view(FieldItemListInterface $items, $langcode = NULL) { 61 | $element = parent::view($items, $langcode); 62 | 63 | $async_urls = []; 64 | foreach ($items as $delta => $item) { 65 | /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $item */ 66 | // Not validating URLs or paths. 67 | $async_urls[] = $item->getUrl()->toString(); 68 | } 69 | 70 | return $this->attachLibraries($element, $async_urls); 71 | } 72 | 73 | /** 74 | * Helper function to attach library definitions and pass JavaScript settings. 75 | * 76 | * @param array $element 77 | * A renderable array of the field element. 78 | * @param array $async_urls 79 | * An array of async Urls. 80 | * 81 | * @return array 82 | * A renderable array of the field element with attached libraries. 83 | */ 84 | private function attachLibraries(array $element, array $async_urls) { 85 | if (!empty($async_urls)) { 86 | $element['#attached'] = [ 87 | 'library' => [ 88 | 'apigee_asyncapi_doc/reactjs', 89 | 'apigee_asyncapi_doc/asyncapi', 90 | ], 91 | ]; 92 | } 93 | return $element; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /modules/apigee_asyncapi_doc/templates/apigee-asyncapi-doc-file-link-field-item.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file apigee-async-doc-file-link-field-item.html.twig 4 | * Copyright 2022 Google Inc. 5 | * 6 | * This program is free software; you can redistribute it and/or modify it under 7 | * the terms of the GNU General Public License version 2 as published by the 8 | * Free Software Foundation. 9 | * 10 | * This program is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 51 17 | * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | */ 19 | 20 | /** 21 | * Default theme implementation for Apigee AsyncAPI Doc field items. 22 | * 23 | * Available variables: 24 | * - field_name: The name of the field. 25 | * - delta: The delta of the field item. 26 | * - link: A link to the file. 27 | * 28 | * @ingroup themeable 29 | */ 30 | #} 31 |
32 | {% set module_path = _self|split('/templates') %} 33 | 36 | 37 |
38 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/apigee_freeform_doc.info.yml: -------------------------------------------------------------------------------- 1 | name: Free-form documentation for Apigee 2 | type: module 3 | description: Free-form documentation for Apigee 4 | package: Apigee (Experimental) 5 | core_version_requirement: ^10 || ^11 6 | dependencies: 7 | - apigee_api_catalog 8 | - file 9 | - text 10 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/apigee_freeform_doc.install: -------------------------------------------------------------------------------- 1 | get('node.type.locked'); 32 | $locked['freeform_doc'] = 'freeform_doc'; 33 | \Drupal::state()->set('node.type.locked', $locked); 34 | } 35 | 36 | /** 37 | * Implements hook_uninstall(). 38 | */ 39 | function apigee_freeform_doc_uninstall() { 40 | // Allow to delete a freeform_doc's node type. 41 | $locked = \Drupal::state()->get('node.type.locked'); 42 | unset($locked['freeform_doc']); 43 | \Drupal::state()->set('node.type.locked', $locked); 44 | } 45 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/apigee_freeform_doc.links.action.yml: -------------------------------------------------------------------------------- 1 | node.freeform_doc.add_form: 2 | route_name: node.add 3 | title: 'Free-form Doc' 4 | route_parameters: 5 | node_type: 'freeform_doc' 6 | appears_on: 7 | - view.api_catalog_admin.page_1 8 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/apigee_freeform_doc.routing.yml: -------------------------------------------------------------------------------- 1 | apigee_freeform_doc.confirm_remove_views_references: 2 | path: '/admin/modules/uninstall/apigee-freeform-doc/confirm-remove-views-references' 3 | defaults: 4 | _title: 'Confirm remove views references' 5 | _form: 'Drupal\apigee_freeform_doc\Form\ConfirmRemoveViewsReferencesForm' 6 | requirements: 7 | _permission: 'administer modules' 8 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/apigee_freeform_doc.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | apigee_freeform_doc.event_subscriber: 3 | class: Drupal\apigee_freeform_doc\EventSubscriber\ApigeeFreeformDocSubscriber 4 | arguments: ['@messenger'] 5 | tags: 6 | - { name: event_subscriber } 7 | 8 | apigee_freeform_doc.uninstall_validator: 9 | class: Drupal\apigee_freeform_doc\ApigeeFreeformDocUninstallValidator 10 | tags: 11 | - { name: module_install.uninstall_validator } 12 | arguments: ['@entity_type.manager', '@string_translation'] 13 | 14 | apigee_freeform_doc.breadcrumb: 15 | class: Drupal\apigee_freeform_doc\ApigeeFreeformDocBreadcrumbBuilder 16 | tags: 17 | - { name: breadcrumb_builder, priority: 1000 } 18 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/install/field.field.node.freeform_doc.body.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.body 6 | - node.type.freeform_doc 7 | module: 8 | - text 9 | id: node.freeform_doc.body 10 | field_name: body 11 | entity_type: node 12 | bundle: freeform_doc 13 | label: 'API Description' 14 | description: 'If the summary is not set, the trimmed API Description will end at the last full sentence before 200 character limit.' 15 | required: false 16 | translatable: true 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | display_summary: true 21 | required_summary: false 22 | field_type: text_with_summary 23 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/install/field.field.node.freeform_doc.field_api_product.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_api_product 6 | - node.type.freeform_doc 7 | id: node.freeform_doc.field_api_product 8 | field_name: field_api_product 9 | entity_type: node 10 | bundle: freeform_doc 11 | label: 'API Product' 12 | description: 'Apigee API Product' 13 | required: false 14 | translatable: false 15 | default_value: { } 16 | default_value_callback: '' 17 | settings: 18 | handler: 'default:api_product' 19 | handler_settings: 20 | target_bundles: null 21 | auto_create: false 22 | field_type: entity_reference 23 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/install/field.field.node.freeform_doc.field_freeform_spec_doc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_freeform_spec_doc 6 | - node.type.freeform_doc 7 | module: 8 | - file 9 | id: node.freeform_doc.field_freeform_spec_doc 10 | field_name: field_freeform_spec_doc 11 | entity_type: node 12 | bundle: freeform_doc 13 | label: 'Supporting docs' 14 | description: 'Upload the supporting doc/attachment here.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | handler: 'default:file' 21 | handler_settings: { } 22 | file_directory: freeform_specs_doc 23 | file_extensions: 'zip jpg jpeg gif png txt html doc xls pdf ppt pps yaml' 24 | max_filesize: '' 25 | description_field: false 26 | field_type: file 27 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/install/field.storage.node.field_freeform_spec_doc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_freeform_doc 7 | module: 8 | - file 9 | - node 10 | id: node.field_freeform_spec_doc 11 | field_name: field_freeform_spec_doc 12 | entity_type: node 13 | type: file 14 | settings: 15 | target_type: file 16 | display_field: false 17 | display_default: false 18 | uri_scheme: public 19 | module: file 20 | locked: false 21 | cardinality: -1 22 | translatable: false 23 | indexes: { } 24 | persist_with_no_fields: false 25 | custom_storage: false 26 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/install/node.type.freeform_doc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - apigee_api_catalog 6 | - apigee_freeform_doc 7 | enforced: 8 | module: 9 | - apigee_freeform_doc 10 | type: freeform_doc 11 | name: 'Free-form Doc' 12 | description: 'Use Free-form Doc content to get Free-form Documentation.' 13 | help: '' 14 | new_revision: false 15 | display_submitted: false 16 | preview_mode: 1 17 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/optional/core.entity_form_display.node.freeform_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.freeform_doc.body 6 | - field.field.node.freeform_doc.field_api_product 7 | - field.field.node.freeform_doc.field_freeform_spec_doc 8 | - node.type.freeform_doc 9 | module: 10 | - file 11 | - path 12 | - text 13 | id: node.freeform_doc.default 14 | targetEntityType: node 15 | bundle: freeform_doc 16 | mode: default 17 | content: 18 | title: 19 | type: string_textfield 20 | weight: 0 21 | region: content 22 | settings: 23 | size: 60 24 | placeholder: '' 25 | third_party_settings: { } 26 | body: 27 | type: text_textarea_with_summary 28 | weight: 2 29 | settings: 30 | rows: 3 31 | summary_rows: 2 32 | placeholder: '' 33 | show_summary: false 34 | third_party_settings: { } 35 | region: content 36 | field_freeform_spec_doc: 37 | type: file_generic 38 | weight: 3 39 | region: content 40 | settings: 41 | progress_indicator: throbber 42 | third_party_settings: { } 43 | field_api_product: 44 | weight: 4 45 | settings: 46 | match_operator: CONTAINS 47 | match_limit: 10 48 | size: 60 49 | placeholder: '' 50 | third_party_settings: { } 51 | type: entity_reference_autocomplete 52 | region: content 53 | created: 54 | type: datetime_timestamp 55 | weight: 5 56 | region: content 57 | settings: { } 58 | third_party_settings: { } 59 | promote: 60 | type: boolean_checkbox 61 | settings: 62 | display_label: true 63 | weight: 6 64 | region: content 65 | third_party_settings: { } 66 | uid: 67 | type: entity_reference_autocomplete 68 | weight: 7 69 | settings: 70 | match_operator: CONTAINS 71 | size: 60 72 | placeholder: '' 73 | match_limit: 10 74 | region: content 75 | third_party_settings: { } 76 | sticky: 77 | type: boolean_checkbox 78 | settings: 79 | display_label: true 80 | weight: 8 81 | region: content 82 | third_party_settings: { } 83 | path: 84 | type: path 85 | weight: 9 86 | region: content 87 | settings: { } 88 | third_party_settings: { } 89 | status: 90 | type: boolean_checkbox 91 | settings: 92 | display_label: true 93 | weight: 10 94 | region: content 95 | third_party_settings: { } 96 | hidden: { } 97 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/config/optional/core.entity_view_display.node.freeform_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.freeform_doc.body 6 | - field.field.node.freeform_doc.field_api_product 7 | - field.field.node.freeform_doc.field_freeform_spec_doc 8 | - node.type.freeform_doc 9 | module: 10 | - file 11 | - text 12 | - user 13 | id: node.freeform_doc.default 14 | targetEntityType: node 15 | bundle: freeform_doc 16 | mode: default 17 | content: 18 | body: 19 | type: text_default 20 | label: hidden 21 | settings: { } 22 | third_party_settings: { } 23 | weight: 1 24 | region: content 25 | field_freeform_spec_doc: 26 | type: file_default 27 | label: hidden 28 | settings: 29 | use_description_as_link_text: true 30 | third_party_settings: { } 31 | weight: 2 32 | region: content 33 | links: 34 | settings: { } 35 | third_party_settings: { } 36 | weight: 0 37 | region: content 38 | hidden: 39 | field_api_product: true 40 | field_image: true 41 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/src/ApigeeFreeformDocBreadcrumbBuilder.php: -------------------------------------------------------------------------------- 1 | getParameter('node'); 42 | return $node instanceof NodeInterface && $node->getType() === 'freeform_doc'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function build(RouteMatchInterface $route_match) { 49 | $breadcrumb = new Breadcrumb(); 50 | 51 | $links[] = Link::createFromRoute($this->t('Home'), ''); 52 | 53 | // API Catalog page is a view. 54 | $links[] = Link::createFromRoute($this->t('API Catalog'), 'view.apigee_api_catalog.page_1'); 55 | 56 | $breadcrumb->setLinks($links); 57 | $breadcrumb->addCacheContexts(['route']); 58 | 59 | return $breadcrumb; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/src/ApigeeFreeformDocUninstallValidator.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_manager; 54 | $this->stringTranslation = $string_translation; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function validate($module) { 61 | $reasons = []; 62 | if ($module == 'apigee_freeform_doc') { 63 | if ($this->hasFreeformDocNodes()) { 64 | $reasons[] = $this->t('To uninstall Apigee Free-form Doc, first delete all Free-form Doc content'); 65 | } 66 | 67 | // Restrict uninstall, if reference of freeform_doc is there in apigee_api_catalog view. 68 | $is_views_references = FALSE; 69 | $view = Views::getView('apigee_api_catalog'); 70 | if (is_object($view)) { 71 | $display = $view->getDisplay(); 72 | 73 | $filters = $view->display_handler->getOption('filters'); 74 | if (isset($filters['type']['value']['freeform_doc'])) { 75 | $is_views_references = TRUE; 76 | } 77 | } 78 | 79 | // Restrict uninstall, if reference of freeform_doc is there in api_catalog_admin view. 80 | $view = Views::getView('api_catalog_admin'); 81 | if (is_object($view)) { 82 | $display = $view->getDisplay(); 83 | 84 | $filters = $view->display_handler->getOption('filters'); 85 | if (isset($filters['type']['value']['freeform_doc'])) { 86 | $is_views_references = TRUE; 87 | } 88 | } 89 | if ($is_views_references) { 90 | $message_arguments = [ 91 | ':url' => Url::fromRoute('apigee_freeform_doc.confirm_remove_views_references')->toString(), 92 | ]; 93 | $reasons[] = $this->t('Click to remove Free-form Doc references from both "apigee_api_catalog" and "api_catalog_admin" views', $message_arguments); 94 | } 95 | } 96 | 97 | return $reasons; 98 | } 99 | 100 | /** 101 | * Determines if there are any freeform_doc nodes or not. 102 | * 103 | * @return bool 104 | * TRUE if there are forum nodes, FALSE otherwise. 105 | */ 106 | protected function hasFreeformDocNodes() { 107 | $nodes = $this->entityTypeManager->getStorage('node')->getQuery() 108 | ->condition('type', 'freeform_doc') 109 | ->accessCheck(FALSE) 110 | ->range(0, 1) 111 | ->execute(); 112 | return !empty($nodes); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/src/EventSubscriber/ApigeeFreeformDocSubscriber.php: -------------------------------------------------------------------------------- 1 | messenger = $messenger; 49 | } 50 | 51 | /** 52 | * Update views on installation. 53 | * 54 | * @param \Drupal\Core\Config\ConfigCrudEvent $event 55 | * Config crud event. 56 | */ 57 | public function configSave(ConfigCrudEvent $event) { 58 | $config = $event->getConfig(); 59 | if ($config->getName() == 'core.entity_view_display.node.freeform_doc.default') { 60 | 61 | $view = Views::getView('apigee_api_catalog'); 62 | if (is_object($view)) { 63 | $display = $view->getDisplay(); 64 | 65 | $filters = $view->display_handler->getOption('filters'); 66 | if ($filters['type']) { 67 | $filters['type']['value']['freeform_doc'] = 'freeform_doc'; 68 | } 69 | if (isset($filters['type_1'])) { 70 | $filters['type_1']['value']['freeform_doc'] = 'freeform_doc'; 71 | } 72 | $view->display_handler->overrideOption('filters', $filters); 73 | 74 | $view->save(); 75 | \Drupal::messenger()->addStatus('Updating Views - API Catalog view'); 76 | } 77 | 78 | $view = Views::getView('api_catalog_admin'); 79 | if (is_object($view)) { 80 | $display = $view->getDisplay(); 81 | 82 | $filters = $view->display_handler->getOption('filters'); 83 | if ($filters['type']) { 84 | $filters['type']['value']['freeform_doc'] = 'freeform_doc'; 85 | } 86 | if (isset($filters['type_1'])) { 87 | $filters['type_1']['value']['freeform_doc'] = 'freeform_doc'; 88 | } 89 | $view->display_handler->overrideOption('filters', $filters); 90 | 91 | $view->save(); 92 | \Drupal::messenger()->addStatus('Updating Views - API Catalog Admin'); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public static function getSubscribedEvents() { 101 | return [ 102 | ConfigEvents::SAVE => 'configSave', 103 | ]; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /modules/apigee_freeform_doc/src/Form/ConfirmRemoveViewsReferencesForm.php: -------------------------------------------------------------------------------- 1 | t('Are you sure you want to remove references of Free-form in views before uninstallation of the module?'); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getCancelUrl() { 51 | return new Url('system.modules_uninstall'); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function submitForm(array &$form, FormStateInterface $form_state) { 58 | $view = Views::getView('apigee_api_catalog'); 59 | if (is_object($view)) { 60 | $display = $view->getDisplay(); 61 | 62 | $filters = $view->display_handler->getOption('filters'); 63 | if ($filters['type']['value']['freeform_doc']) { 64 | unset($filters['type']['value']['freeform_doc']); 65 | } 66 | if (isset($filters['type_1']) && $filters['type_1']['value']['freeform_doc']) { 67 | unset($filters['type_1']['value']['freeform_doc']); 68 | } 69 | $view->display_handler->overrideOption('filters', $filters); 70 | 71 | $view->save(); 72 | 73 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog')); 74 | } 75 | 76 | $view = Views::getView('api_catalog_admin'); 77 | if (is_object($view)) { 78 | $display = $view->getDisplay(); 79 | 80 | $filters = $view->display_handler->getOption('filters'); 81 | if ($filters['type']['value']['freeform_doc']) { 82 | unset($filters['type']['value']['freeform_doc']); 83 | } 84 | if (isset($filters['type_1']) && $filters['type_1']['value']['freeform_doc']) { 85 | unset($filters['type_1']['value']['freeform_doc']); 86 | } 87 | $view->display_handler->overrideOption('filters', $filters); 88 | 89 | $view->save(); 90 | 91 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog Admin')); 92 | } 93 | 94 | $form_state->setRedirectUrl($this->getCancelUrl()); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.info.yml: -------------------------------------------------------------------------------- 1 | name: GraphQL for Apigee 2 | type: module 3 | description: GraphQL for Apigee 4 | package: Apigee (Experimental) 5 | core_version_requirement: ^10 || ^11 6 | dependencies: 7 | - apigee_api_catalog 8 | - file_link 9 | - text 10 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.install: -------------------------------------------------------------------------------- 1 | get('node.type.locked'); 32 | $locked['graphql_doc'] = 'graphql_doc'; 33 | \Drupal::state()->set('node.type.locked', $locked); 34 | } 35 | 36 | /** 37 | * Implements hook_uninstall(). 38 | */ 39 | function apigee_graphql_doc_uninstall() { 40 | // Allow to delete a graphql_doc's node type. 41 | $locked = \Drupal::state()->get('node.type.locked'); 42 | unset($locked['graphql_doc']); 43 | \Drupal::state()->set('node.type.locked', $locked); 44 | } 45 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.libraries.yml: -------------------------------------------------------------------------------- 1 | # Integration module library for GraphQL plugin. 2 | apigee_graphql_doc_integration: 3 | version: 1.0 4 | js: 5 | js/apigee-graphql-doc.js: {} 6 | css: 7 | component: 8 | css/apigee-graphql-doc.css: {} 9 | dependencies: 10 | - core/jquery 11 | - core/drupalSettings 12 | - apigee_graphql_doc/reactjs 13 | - apigee_graphql_doc/graphiql 14 | 15 | # Third-party library (CDN). 16 | reactjs: 17 | remote: https://reactjs.org 18 | version: VERSION 19 | license: 20 | name: MIT 21 | url: https://github.com/facebook/react/blob/main/LICENSE 22 | gpl-compatible: true 23 | js: 24 | https://unpkg.com/react/umd/react.production.min.js: {type: external, minified: true} 25 | https://unpkg.com/react-dom/umd/react-dom.production.min.js: {type: external, minified: true} 26 | 27 | # Third-party library (CDN). 28 | graphiql: 29 | remote: https://graphql.org 30 | version: VERSION 31 | license: 32 | name: MIT 33 | url: https://github.com/graphql/graphiql/blob/main/LICENSE 34 | gpl-compatible: true 35 | js: 36 | https://unpkg.com/graphiql/graphiql.min.js: {type: external, minified: true} 37 | css: 38 | component: 39 | https://unpkg.com/graphiql/graphiql.min.css: {type: external, minified: true} 40 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.links.action.yml: -------------------------------------------------------------------------------- 1 | node.graphql_doc.add_form: 2 | route_name: node.add 3 | title: 'GraphQL Doc' 4 | route_parameters: 5 | node_type: 'graphql_doc' 6 | appears_on: 7 | - view.api_catalog_admin.page_1 8 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.module: -------------------------------------------------------------------------------- 1 | [ 36 | 'variables' => [ 37 | 'field_name' => NULL, 38 | 'delta' => NULL, 39 | ], 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * Implements hook_ENTITY_TYPE_load(). 46 | */ 47 | function apigee_graphql_doc_node_load($entities) { 48 | foreach ($entities as $entity) { 49 | if ($entity->bundle() != 'graphql_doc') { 50 | return; 51 | } 52 | if ($entity->get('field_graphql_spec_source_type')->value === 'file') { 53 | $spec_value = $entity->get('field_graphql_spec')->isEmpty() ? [] : $entity->get('field_graphql_spec')->getValue()[0]; 54 | if (!empty($spec_value['target_id'])) { 55 | /* @var \Drupal\file\Entity\File $file */ 56 | $file = \Drupal::entityTypeManager() 57 | ->getStorage('file') 58 | ->load($spec_value['target_id']); 59 | 60 | if ($file) { 61 | $path = '/node/' . $entity->id() . '/graphql'; 62 | $entity->set('field_graphql_spec_file_link', ['uri' => Url::fromUri('base:' . $path , ['absolute' => TRUE])->toString()]); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Implements hook_form_FORM_ID_alter(). 71 | */ 72 | function apigee_graphql_doc_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { 73 | $node = $form_state->getFormObject()->getEntity(); 74 | if ($node->bundle() != 'graphql_doc') { 75 | return; 76 | } 77 | 78 | $form['field_graphql_spec']['#states'] = [ 79 | 'visible' => [ 80 | ':input[name="field_graphql_spec_source_type"]' => ['value' => 'file'], 81 | ], 82 | ]; 83 | 84 | // @todo: #states does not work on managed files. 85 | // @see: https://www.drupal.org/project/drupal/issues/2847425 86 | $form['field_graphql_spec']['widget'][0]['#states'] = [ 87 | 'required' => [ 88 | ':input[name="field_graphql_spec_source_type"]' => ['value' => 'file'], 89 | ], 90 | ]; 91 | 92 | $form['field_graphql_spec_file_link']['#states'] = [ 93 | 'visible' => [ 94 | [':input[name="field_graphql_spec_source_type"]' => ['value' => 'url']], 95 | ], 96 | ]; 97 | 98 | $form['field_graphql_spec_file_link']['widget'][0]['uri']['#states'] = [ 99 | 'required' => [ 100 | [':input[name="field_graphql_spec_source_type"]' => ['value' => 'url']], 101 | ], 102 | ]; 103 | 104 | $form['#validate'][] = '_apigee_graphql_form_node_form_validate'; 105 | } 106 | 107 | /** 108 | * Form validator for the "graphql_doc" node bundle. 109 | * 110 | * @param array $form 111 | * The form. 112 | * @param \Drupal\Core\Form\FormStateInterface $form_state 113 | * The form state. 114 | */ 115 | function _apigee_graphql_form_node_form_validate(&$form, FormStateInterface $form_state) { 116 | $values = $form_state->getValues(); 117 | 118 | if (empty($values['field_graphql_spec_source_type'])) { 119 | return; 120 | } 121 | 122 | // Make sure the field_graphql_spec (file) or field_graphql_spec_file_link (link) 123 | // is not empty, according to what was selected as the file source. 124 | $source = $values['field_graphql_spec_source_type'][0]['value'] ?: NULL; 125 | if ($source == 'file' && empty($values['field_graphql_spec'][0]['fids'][0])) { 126 | $form_state->setErrorByName('field_graphql_spec', t('Provide an GraphQL specification file.')); 127 | } 128 | elseif ($source == 'url' && empty($values['field_graphql_spec_file_link'][0]['uri'])) { 129 | $form_state->setErrorByName('field_graphql_spec_file_link', t('Provide the URL to an GraphQL specification file.')); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.routing.yml: -------------------------------------------------------------------------------- 1 | apigee_graphql_doc.confirm_remove_views_references: 2 | path: '/admin/modules/uninstall/apigee-graphql-doc/confirm-remove-views-references' 3 | defaults: 4 | _title: 'Confirm remove views references' 5 | _form: 'Drupal\apigee_graphql_doc\Form\ConfirmRemoveViewsReferencesForm' 6 | requirements: 7 | _permission: 'administer modules' 8 | 9 | entity.node.graphqlserver: 10 | path: '/node/{node}/graphql' 11 | defaults: 12 | _title: 'Mock graphql server' 13 | _controller: '\Drupal\apigee_graphql_doc\Controller\ApigeeGraphqlServerController::build' 14 | methods: [POST] 15 | requirements: 16 | _permission: 'access content' 17 | node: \d+ 18 | options: 19 | node: 20 | type: entity:node 21 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/apigee_graphql_doc.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | apigee_graphql_doc.event_subscriber: 3 | class: Drupal\apigee_graphql_doc\EventSubscriber\ApigeeGraphqlDocSubscriber 4 | arguments: ['@messenger'] 5 | tags: 6 | - { name: event_subscriber } 7 | 8 | apigee_graphql_doc.uninstall_validator: 9 | class: Drupal\apigee_graphql_doc\ApigeeGraphqlDocUninstallValidator 10 | tags: 11 | - { name: module_install.uninstall_validator } 12 | arguments: ['@entity_type.manager', '@string_translation'] 13 | 14 | apigee_graphql_doc.breadcrumb: 15 | class: Drupal\apigee_graphql_doc\ApigeeGraphqlDocBreadcrumbBuilder 16 | tags: 17 | - { name: breadcrumb_builder, priority: 1000 } 18 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.field.node.graphql_doc.body.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.body 6 | - node.type.graphql_doc 7 | module: 8 | - text 9 | id: node.graphql_doc.body 10 | field_name: body 11 | entity_type: node 12 | bundle: graphql_doc 13 | label: Description 14 | description: '' 15 | required: false 16 | translatable: true 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | display_summary: true 21 | required_summary: false 22 | field_type: text_with_summary 23 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.field.node.graphql_doc.field_api_product.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_api_product 6 | - node.type.graphql_doc 7 | id: node.graphql_doc.field_api_product 8 | field_name: field_api_product 9 | entity_type: node 10 | bundle: graphql_doc 11 | label: 'API Product' 12 | description: 'Apigee API Product' 13 | required: false 14 | translatable: false 15 | default_value: { } 16 | default_value_callback: '' 17 | settings: 18 | handler: 'default:api_product' 19 | handler_settings: 20 | target_bundles: null 21 | auto_create: false 22 | field_type: entity_reference 23 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.field.node.graphql_doc.field_graphql_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_graphql_spec 6 | - node.type.graphql_doc 7 | module: 8 | - file 9 | id: node.graphql_doc.field_graphql_spec 10 | field_name: field_graphql_spec 11 | entity_type: node 12 | bundle: graphql_doc 13 | label: 'GraphQL specification' 14 | description: 'The spec snapshot. Please note that it will be rendered by mock server and only schema will be accessible.' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | handler: 'default:file' 21 | handler_settings: { } 22 | file_directory: graphql_specs 23 | file_extensions: 'graphql' 24 | max_filesize: '' 25 | description_field: false 26 | field_type: file -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.field.node.graphql_doc.field_graphql_spec_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_graphql_spec_file_link 6 | - node.type.graphql_doc 7 | module: 8 | - file_link 9 | id: node.graphql_doc.field_graphql_spec_file_link 10 | field_name: field_graphql_spec_file_link 11 | entity_type: node 12 | bundle: graphql_doc 13 | label: 'GraphQL Endpoint' 14 | description: 'Enter the the GraphQL endpoint' 15 | required: false 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: 20 | link_type: 17 21 | title: 0 22 | file_extensions: '' 23 | no_extension: true 24 | deferred_request: false 25 | field_type: file_link 26 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.field.node.graphql_doc.field_graphql_spec_source_type.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.storage.node.field_graphql_spec_source_type 6 | - node.type.graphql_doc 7 | module: 8 | - options 9 | id: node.graphql_doc.field_graphql_spec_source_type 10 | field_name: field_graphql_spec_source_type 11 | entity_type: node 12 | bundle: graphql_doc 13 | label: 'Specification source type' 14 | description: 'Indicate if the GraphQL spec will be provided as a file for upload or a URL.' 15 | required: true 16 | translatable: false 17 | default_value: { } 18 | default_value_callback: '' 19 | settings: { } 20 | field_type: list_string 21 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.storage.node.field_graphql_spec.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_graphql_doc 7 | module: 8 | - file 9 | - node 10 | id: node.field_graphql_spec 11 | field_name: field_graphql_spec 12 | entity_type: node 13 | type: file 14 | settings: 15 | target_type: file 16 | display_field: false 17 | display_default: false 18 | uri_scheme: public 19 | module: file 20 | locked: false 21 | cardinality: 1 22 | translatable: false 23 | indexes: { } 24 | persist_with_no_fields: false 25 | custom_storage: false 26 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.storage.node.field_graphql_spec_file_link.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_graphql_doc 7 | module: 8 | - file_link 9 | - node 10 | id: node.field_graphql_spec_file_link 11 | field_name: field_graphql_spec_file_link 12 | entity_type: node 13 | type: file_link 14 | settings: { } 15 | module: file_link 16 | locked: false 17 | cardinality: 1 18 | translatable: true 19 | indexes: { } 20 | persist_with_no_fields: false 21 | custom_storage: false 22 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/field.storage.node.field_graphql_spec_source_type.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | enforced: 5 | module: 6 | - apigee_graphql_doc 7 | module: 8 | - node 9 | - options 10 | id: node.field_graphql_spec_source_type 11 | field_name: field_graphql_spec_source_type 12 | entity_type: node 13 | type: list_string 14 | settings: 15 | allowed_values: 16 | - 17 | value: file 18 | label: File 19 | - 20 | value: url 21 | label: URL 22 | allowed_values_function: '' 23 | module: options 24 | locked: false 25 | cardinality: 1 26 | translatable: false 27 | indexes: { } 28 | persist_with_no_fields: false 29 | custom_storage: false 30 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/install/node.type.graphql_doc.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | dependencies: 3 | module: 4 | - apigee_api_catalog 5 | - apigee_graphql_doc 6 | enforced: 7 | module: 8 | - apigee_graphql_doc 9 | type: graphql_doc 10 | name: 'GraphQL Doc' 11 | description: 'Use GraphQL Doc content to get GraphQL Doc.' 12 | help: '' 13 | new_revision: false 14 | display_submitted: false 15 | preview_mode: 0 16 | status: true 17 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/optional/core.entity_form_display.node.graphql_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.graphql_doc.body 6 | - field.field.node.graphql_doc.field_api_product 7 | - field.field.node.graphql_doc.field_graphql_spec 8 | - field.field.node.graphql_doc.field_graphql_spec_file_link 9 | - field.field.node.graphql_doc.field_graphql_spec_source_type 10 | - node.type.graphql_doc 11 | module: 12 | - file_link 13 | - path 14 | - text 15 | id: node.graphql_doc.default 16 | targetEntityType: node 17 | bundle: graphql_doc 18 | mode: default 19 | content: 20 | title: 21 | type: string_textfield 22 | weight: 0 23 | region: content 24 | settings: 25 | size: 60 26 | placeholder: '' 27 | third_party_settings: { } 28 | body: 29 | type: text_textarea_with_summary 30 | weight: 1 31 | settings: 32 | rows: 9 33 | summary_rows: 3 34 | placeholder: '' 35 | show_summary: false 36 | third_party_settings: { } 37 | region: content 38 | field_graphql_spec_source_type: 39 | type: options_select 40 | weight: 4 41 | region: content 42 | settings: { } 43 | third_party_settings: { } 44 | field_graphql_spec: 45 | type: file_generic 46 | weight: 5 47 | region: content 48 | settings: 49 | progress_indicator: throbber 50 | third_party_settings: { } 51 | field_graphql_spec_file_link: 52 | type: file_link_default 53 | weight: 6 54 | region: content 55 | settings: 56 | placeholder_url: '' 57 | placeholder_title: '' 58 | third_party_settings: { } 59 | field_api_product: 60 | weight: 7 61 | settings: 62 | match_operator: CONTAINS 63 | match_limit: 10 64 | size: 60 65 | placeholder: '' 66 | third_party_settings: { } 67 | type: entity_reference_autocomplete 68 | region: content 69 | created: 70 | type: datetime_timestamp 71 | weight: 8 72 | region: content 73 | settings: { } 74 | third_party_settings: { } 75 | promote: 76 | type: boolean_checkbox 77 | settings: 78 | display_label: true 79 | weight: 9 80 | region: content 81 | third_party_settings: { } 82 | uid: 83 | type: entity_reference_autocomplete 84 | weight: 10 85 | settings: 86 | match_operator: CONTAINS 87 | size: 60 88 | placeholder: '' 89 | match_limit: 10 90 | region: content 91 | third_party_settings: { } 92 | sticky: 93 | type: boolean_checkbox 94 | settings: 95 | display_label: true 96 | weight: 11 97 | region: content 98 | third_party_settings: { } 99 | path: 100 | type: path 101 | weight: 12 102 | region: content 103 | settings: { } 104 | third_party_settings: { } 105 | status: 106 | type: boolean_checkbox 107 | settings: 108 | display_label: true 109 | weight: 13 110 | region: content 111 | third_party_settings: { } 112 | hidden: { } 113 | 114 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/config/optional/core.entity_view_display.node.graphql_doc.default.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | config: 5 | - field.field.node.graphql_doc.body 6 | - field.field.node.graphql_doc.field_api_product 7 | - field.field.node.graphql_doc.field_graphql_spec 8 | - field.field.node.graphql_doc.field_graphql_spec_file_link 9 | - field.field.node.graphql_doc.field_graphql_spec_source_type 10 | - node.type.graphql_doc 11 | module: 12 | - file_link 13 | - text 14 | - user 15 | id: node.graphql_doc.default 16 | targetEntityType: node 17 | bundle: graphql_doc 18 | mode: default 19 | content: 20 | body: 21 | label: hidden 22 | type: text_default 23 | weight: 1 24 | settings: { } 25 | third_party_settings: { } 26 | region: content 27 | field_graphql_spec_file_link: 28 | weight: 3 29 | label: hidden 30 | settings: 31 | trim_length: 80 32 | url_only: true 33 | url_plain: false 34 | rel: '' 35 | target: '' 36 | format_size: true 37 | third_party_settings: { } 38 | type: apigee_graphql_doc_graphiql 39 | region: content 40 | links: 41 | weight: 0 42 | region: content 43 | settings: { } 44 | third_party_settings: { } 45 | hidden: 46 | field_api_product: true 47 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/css/apigee-graphql-doc.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License version 2 as published by the 6 | * Free Software Foundation. 7 | * 8 | * This program is distributed in the hope that it will be useful, but WITHOUT 9 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 10 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 11 | * License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License along 14 | * with this program; if not, write to the Free Software Foundation, Inc., 51 15 | * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | */ 17 | 18 | .graphiql-container { 19 | height: 99vh; 20 | } 21 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/js/apigee-graphql-doc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License version 2 as published by the 6 | * Free Software Foundation. 7 | * 8 | * This program is distributed in the hope that it will be useful, but WITHOUT 9 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 10 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 11 | * License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License along 14 | * with this program; if not, write to the Free Software Foundation, Inc., 51 15 | * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | */ 17 | 18 | /** 19 | * @file 20 | * Custom scripts to render file fields with Apigee Graphql Doc. 21 | */ 22 | 23 | (function ($, window, Drupal, drupalSettings) { 24 | 25 | Drupal.behaviors.apigeeGraphiqlFormatter = { 26 | attach: function(context) { 27 | // Iterate over fields and render each field item. 28 | for (var fieldName in drupalSettings.apigeeGraphqlDocFormatter) { 29 | if (drupalSettings.apigeeGraphqlDocFormatter.hasOwnProperty(fieldName)) { 30 | var field = drupalSettings.apigeeGraphqlDocFormatter[fieldName]; 31 | for (var fieldDelta = 0; fieldDelta < field.graphqlUrls.length; fieldDelta++) { 32 | ReactDOM.render( 33 | React.createElement(GraphiQL, { 34 | fetcher: GraphiQL.createFetcher({ url: field.graphqlUrls[fieldDelta] }), 35 | defaultSecondaryEditorOpen: true, 36 | headerEditorEnabled: true, 37 | shouldPersistHeaders: true, 38 | }), 39 | document.getElementById(`graphql-explorer-${fieldName}-${fieldDelta}`), 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | }; 46 | 47 | }(jQuery, window, Drupal, drupalSettings)); 48 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/ApigeeGraphqlDocBreadcrumbBuilder.php: -------------------------------------------------------------------------------- 1 | getParameter('node'); 42 | return $node instanceof NodeInterface && $node->getType() === 'graphql_doc'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function build(RouteMatchInterface $route_match) { 49 | $breadcrumb = new Breadcrumb(); 50 | 51 | $links[] = Link::createFromRoute($this->t('Home'), ''); 52 | 53 | // API Catalog page is a view. 54 | $links[] = Link::createFromRoute($this->t('API Catalog'), 'view.apigee_api_catalog.page_1'); 55 | 56 | $breadcrumb->setLinks($links); 57 | $breadcrumb->addCacheContexts(['route']); 58 | 59 | return $breadcrumb; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/ApigeeGraphqlDocUninstallValidator.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_manager; 54 | $this->stringTranslation = $string_translation; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function validate($module) { 61 | $reasons = []; 62 | if ($module == 'apigee_graphql_doc') { 63 | if ($this->hasGraphqlDocNodes()) { 64 | $reasons[] = $this->t('To uninstall Apigee GraphQL Doc, first delete all GraphQL Doc content'); 65 | } 66 | 67 | // Restrict uninstall, if reference of graphql_doc is there in apigee_api_catalog view. 68 | $is_views_references = FALSE; 69 | $view = Views::getView('apigee_api_catalog'); 70 | if (is_object($view)) { 71 | $display = $view->getDisplay(); 72 | 73 | $filters = $view->display_handler->getOption('filters'); 74 | if (isset($filters['type']['value']['graphql_doc'])) { 75 | $is_views_references = TRUE; 76 | } 77 | } 78 | 79 | // Restrict uninstall, if reference of graphql_doc is there in api_catalog_admin view. 80 | $view = Views::getView('api_catalog_admin'); 81 | if (is_object($view)) { 82 | $display = $view->getDisplay(); 83 | 84 | $filters = $view->display_handler->getOption('filters'); 85 | if (isset($filters['type']['value']['graphql_doc'])) { 86 | $is_views_references = TRUE; 87 | } 88 | } 89 | if ($is_views_references) { 90 | $message_arguments = [ 91 | ':url' => Url::fromRoute('apigee_graphql_doc.confirm_remove_views_references')->toString(), 92 | ]; 93 | $reasons[] = $this->t('Click to remove GraphQL Doc references from both "apigee_api_catalog" and "api_catalog_admin" views', $message_arguments); 94 | } 95 | } 96 | 97 | return $reasons; 98 | } 99 | 100 | /** 101 | * Determines if there are any graphql_doc nodes or not. 102 | * 103 | * @return bool 104 | * TRUE if there are forum nodes, FALSE otherwise. 105 | */ 106 | protected function hasGraphqlDocNodes() { 107 | $nodes = $this->entityTypeManager->getStorage('node')->getQuery() 108 | ->condition('type', 'graphql_doc') 109 | ->accessCheck(FALSE) 110 | ->range(0, 1) 111 | ->execute(); 112 | return !empty($nodes); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/Controller/ApigeeGraphqlServerController.php: -------------------------------------------------------------------------------- 1 | bundle() != 'graphql_doc' && $node->get('field_graphql_spec_source_type')->value != 'file') { 50 | return new JsonResponse('Error'); 51 | } 52 | $field_graphql_spec = $node->get('field_graphql_spec'); 53 | 54 | if ($field_graphql_spec && $field_graphql_spec->target_id) { 55 | /** @var \Drupal\file\Entity\File $file */ 56 | $file = File::load($field_graphql_spec->target_id); 57 | 58 | if ($file) { 59 | $schema_url = $file->getFileUri(); 60 | $contents = file_get_contents($schema_url); 61 | $schema = BuildSchema::build($contents); 62 | 63 | $rawInput = \Drupal::request()->getContent(); 64 | $input = Json::decode((string) $rawInput); 65 | $query = $input['query']; 66 | $variableValues = $input['variables'] ?? NULL; 67 | 68 | $rootValue = ['prefix' => 'You said: ']; 69 | $result = GraphQL::executeQuery($schema, $query, $rootValue, NULL, $variableValues)->toArray(); 70 | return new JsonResponse($result); 71 | } 72 | } 73 | 74 | } 75 | catch (Throwable $e) { 76 | $output = [ 77 | 'error' => [ 78 | 'message' => $e->getMessage(), 79 | ], 80 | ]; 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/EventSubscriber/ApigeeGraphqlDocSubscriber.php: -------------------------------------------------------------------------------- 1 | messenger = $messenger; 49 | } 50 | 51 | /** 52 | * Update views on installation. 53 | * 54 | * @param \Drupal\Core\Config\ConfigCrudEvent $event 55 | * Config crud event. 56 | */ 57 | public function configSave(ConfigCrudEvent $event) { 58 | $config = $event->getConfig(); 59 | if ($config->getName() == 'core.entity_view_display.node.graphql_doc.default') { 60 | 61 | $view = Views::getView('apigee_api_catalog'); 62 | if (is_object($view)) { 63 | $display = $view->getDisplay(); 64 | 65 | $filters = $view->display_handler->getOption('filters'); 66 | if ($filters['type']) { 67 | $filters['type']['value']['graphql_doc'] = 'graphql_doc'; 68 | } 69 | if (isset($filters['type_1'])) { 70 | $filters['type_1']['value']['graphql_doc'] = 'graphql_doc'; 71 | } 72 | $view->display_handler->overrideOption('filters', $filters); 73 | 74 | $view->save(); 75 | \Drupal::messenger()->addStatus('Updating Views - API Catalog view'); 76 | } 77 | 78 | $view = Views::getView('api_catalog_admin'); 79 | if (is_object($view)) { 80 | $display = $view->getDisplay(); 81 | 82 | $filters = $view->display_handler->getOption('filters'); 83 | if ($filters['type']) { 84 | $filters['type']['value']['graphql_doc'] = 'graphql_doc'; 85 | } 86 | if (isset($filters['type_1'])) { 87 | $filters['type_1']['value']['graphql_doc'] = 'graphql_doc'; 88 | } 89 | $view->display_handler->overrideOption('filters', $filters); 90 | 91 | $view->save(); 92 | \Drupal::messenger()->addStatus('Updating Views - API Catalog Admin'); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public static function getSubscribedEvents() { 101 | return [ 102 | ConfigEvents::SAVE => 'configSave', 103 | ]; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/Form/ConfirmRemoveViewsReferencesForm.php: -------------------------------------------------------------------------------- 1 | t('Are you sure you want to remove references of GraphQL in views before uninstallation of the module?'); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getCancelUrl() { 51 | return new Url('system.modules_uninstall'); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function submitForm(array &$form, FormStateInterface $form_state) { 58 | $view = Views::getView('apigee_api_catalog'); 59 | if (is_object($view)) { 60 | $display = $view->getDisplay(); 61 | 62 | $filters = $view->display_handler->getOption('filters'); 63 | if ($filters['type']['value']['graphql_doc']) { 64 | unset($filters['type']['value']['graphql_doc']); 65 | } 66 | if (isset($filters['type_1']) && $filters['type_1']['value']['graphql_doc']) { 67 | unset($filters['type_1']['value']['graphql_doc']); 68 | } 69 | $view->display_handler->overrideOption('filters', $filters); 70 | 71 | $view->save(); 72 | 73 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog')); 74 | } 75 | 76 | $view = Views::getView('api_catalog_admin'); 77 | if (is_object($view)) { 78 | $display = $view->getDisplay(); 79 | 80 | $filters = $view->display_handler->getOption('filters'); 81 | if ($filters['type']['value']['graphql_doc']) { 82 | unset($filters['type']['value']['graphql_doc']); 83 | } 84 | if (isset($filters['type_1']) && $filters['type_1']['value']['graphql_doc']) { 85 | unset($filters['type_1']['value']['graphql_doc']); 86 | } 87 | $view->display_handler->overrideOption('filters', $filters); 88 | 89 | $view->save(); 90 | 91 | $this->messenger()->addStatus($this->t('Updating Views - API Catalog Admin')); 92 | } 93 | 94 | $form_state->setRedirectUrl($this->getCancelUrl()); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/src/Plugin/Field/FieldFormatter/GraphiqlFormatter.php: -------------------------------------------------------------------------------- 1 | $item) { 46 | $element[$delta] = [ 47 | '#theme' => 'apigee_graphql_doc_file_link_field_item', 48 | '#field_name' => $this->fieldDefinition->getName(), 49 | '#delta' => $delta, 50 | ]; 51 | } 52 | 53 | return $element; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function view(FieldItemListInterface $items, $langcode = NULL) { 60 | $element = parent::view($items, $langcode); 61 | 62 | $graphql_urls = []; 63 | foreach ($items as $delta => $item) { 64 | /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $item */ 65 | // Not validating URLs or paths. 66 | $graphql_urls[] = $item->getUrl()->toString(); 67 | } 68 | 69 | return $this->attachLibraries($element, $graphql_urls); 70 | } 71 | 72 | /** 73 | * Helper function to attach library definitions and pass JavaScript settings. 74 | * 75 | * @param array $element 76 | * A renderable array of the field element. 77 | * @param array $graphql_urls 78 | * An array of GraphQL Urls. 79 | * 80 | * @return array 81 | * A renderable array of the field element with attached libraries. 82 | */ 83 | private function attachLibraries(array $element, array $graphql_urls) { 84 | if (!empty($graphql_urls)) { 85 | $element['#attached'] = [ 86 | 'library' => [ 87 | 'apigee_graphql_doc/reactjs', 88 | 'apigee_graphql_doc/graphiql', 89 | 'apigee_graphql_doc/apigee_graphql_doc_integration', 90 | ], 91 | 'drupalSettings' => [ 92 | 'apigeeGraphqlDocFormatter' => [ 93 | $this->fieldDefinition->getName() => [ 94 | 'graphqlUrls' => $graphql_urls, 95 | ], 96 | ], 97 | ], 98 | ]; 99 | } 100 | return $element; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /modules/apigee_graphql_doc/templates/apigee-graphql-doc-file-link-field-item.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file apigee-graphql-doc-file-link-field-item.html.twig 4 | * Copyright 2021 Google Inc. 5 | * 6 | * This program is free software; you can redistribute it and/or modify it under 7 | * the terms of the GNU General Public License version 2 as published by the 8 | * Free Software Foundation. 9 | * 10 | * This program is distributed in the hope that it will be useful, but WITHOUT 11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 13 | * License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 51 17 | * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | */ 19 | 20 | /** 21 | * Default theme implementation for Apigee GraphQL Doc field items. 22 | * 23 | * Available variables: 24 | * - field_name: The name of the field. 25 | * - delta: The delta of the field item. 26 | * 27 | * @ingroup themeable 28 | */ 29 | #} 30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | */.git/* 5 | */config/* 6 | */css/* 7 | */js/* 8 | */vendor/* 9 | \.md 10 | 11 | 12 | 13 | 14 | 15 | 0 16 | 17 | 18 | 19 | 0 20 | 21 | 25 | 26 | 0 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /phpunit.core.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ./tests/TestSuites/UnitTestSuite.php 46 | 47 | 48 | ./tests/TestSuites/KernelTestSuite.php 49 | 50 | 51 | ./tests/TestSuites/FunctionalTestSuite.php 52 | 53 | 54 | ./tests/TestSuites/FunctionalJavascriptTestSuite.php 55 | 56 | 57 | ./tests/TestSuites/BuildTestSuite.php 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ../modules/contrib/apigee_api_catalog 71 | 72 | 73 | ../modules/contrib/apigee_api_catalog/tests 74 | ../modules/contrib/apigee_api_catalog/test_modules 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/ApigeeApiCatalogBreadcrumbBuilder.php: -------------------------------------------------------------------------------- 1 | getParameter('node'); 42 | return $node instanceof NodeInterface && $node->getType() === 'apidoc'; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function build(RouteMatchInterface $route_match) { 49 | $breadcrumb = new Breadcrumb(); 50 | 51 | $links[] = Link::createFromRoute($this->t('Home'), ''); 52 | 53 | // API Catalog is a view. 54 | $links[] = Link::createFromRoute($this->t('API Catalog'), 'view.apigee_api_catalog.page_1'); 55 | 56 | $breadcrumb->setLinks($links); 57 | $breadcrumb->addCacheContexts(['route']); 58 | 59 | return $breadcrumb; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Entity/Form/ApiDocReimportSpecForm.php: -------------------------------------------------------------------------------- 1 | messenger = $messenger; 76 | $this->specFetcher = $spec_fetcher; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public static function create(ContainerInterface $container) { 83 | return new static( 84 | $container->get('entity.repository'), 85 | $container->get('entity_type.bundle.info'), 86 | $container->get('datetime.time'), 87 | $container->get('messenger'), 88 | $container->get('apigee_api_catalog.spec_fetcher') 89 | ); 90 | } 91 | 92 | /** 93 | * Checks access for the form page. 94 | * 95 | * @param \Drupal\Core\Routing\RouteMatchInterface $route_match 96 | * The route match. 97 | * @param \Drupal\Core\Session\AccountInterface $account 98 | * Run access checks for this account. 99 | * 100 | * @return \Drupal\Core\Access\AccessResultInterface 101 | * The access result. 102 | */ 103 | public function checkAccess(RouteMatchInterface $route_match, AccountInterface $account) { 104 | /** @var \Drupal\node\NodeInterface $entity */ 105 | $entity = $route_match->getParameter('node'); 106 | 107 | return AccessResult::allowedIf($entity->bundle() == 'apidoc' && $entity->access('update', $account)); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function getQuestion() { 114 | return $this->t('Are you sure you want to update the OpenAPI specification file from URL on API Doc %name?', [ 115 | '%name' => $this->entity->label(), 116 | ]); 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | public function getCancelUrl() { 123 | return new Url('view.api_catalog_admin.page_1'); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function getDescription() { 130 | return $this->t('This will replace the current OpenAPI specification file. 131 | This action cannot be undone.'); 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function submitForm(array &$form, FormStateInterface $form_state) { 138 | /** @var \Drupal\node\NodeInterface $entity */ 139 | $entity = $this->getEntity(); 140 | 141 | if ($entity->get('field_apidoc_spec_file_source')->value != SpecFetcherInterface::SPEC_AS_URL) { 142 | $this->messenger()->addStatus($this->t('API Doc %label is using a local file as the the source, there nothing to import.', [ 143 | '%label' => $this->entity->label(), 144 | ])); 145 | 146 | return; 147 | } 148 | 149 | $fetch_status = $this->specFetcher->fetchSpec($entity); 150 | // If STATUS_ERROR the error is displayed already by the fetchSpec() method. 151 | if ($fetch_status != SpecFetcherInterface::STATUS_ERROR) { 152 | if ($entity->getEntityType()->isRevisionable()) { 153 | $entity->setNewRevision(); 154 | } 155 | $entity->save(); 156 | $this->messenger()->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ 157 | '%label' => $this->entity->label(), 158 | ])); 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/EventSubscriber/PageNotFoundEventSubscriber.php: -------------------------------------------------------------------------------- 1 | pathValidator = $path_validator; 62 | $this->pathMatcher = $path_matcher; 63 | } 64 | 65 | /** 66 | * Redirects to the apidoc canonical route if we have a not found exception. 67 | * 68 | * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event 69 | * The exception event. 70 | */ 71 | public function onNotFoundException(ExceptionEvent $event) { 72 | // Check if the request uri matches an apidoc canonical route. 73 | // Also check for apidoc valid path. 74 | if ($event->getThrowable() instanceof NotFoundHttpException 75 | && ($uri = $event->getRequest()->getRequestUri()) 76 | && $this->pathMatcher->matchPath($uri, '/api/*/*') 77 | && ([$apitrail, $api, $id] = explode('/', $uri)) 78 | && ($path = "/api/{$id}") 79 | && $this->pathValidator->isValid($path) 80 | ) { 81 | // Redirect to the apidoc. 82 | $event->setResponse(new RedirectResponse($path)); 83 | } 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public static function getSubscribedEvents() { 90 | $events[KernelEvents::EXCEPTION][] = ['onNotFoundException', 0]; 91 | return $events; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Plugin/Field/FieldFormatter/SmartDocsFormatter.php: -------------------------------------------------------------------------------- 1 | loggerFactory = $loggerFactory->get('apigee_api_catalog'); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 79 | return new static( 80 | $plugin_id, 81 | $plugin_definition, 82 | $configuration['field_definition'], 83 | $configuration['settings'], 84 | $configuration['label'], 85 | $configuration['view_mode'], 86 | $configuration['third_party_settings'], 87 | $container->get('logger.factory') 88 | ); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function view(FieldItemListInterface $items, $langcode = NULL) { 95 | $elements = parent::view($items, $langcode); 96 | 97 | // Add base tag for SmartDocs Angular application. 98 | $xuacompatible = [ 99 | '#tag' => 'base', 100 | '#attributes' => [ 101 | 'href' => base_path(), 102 | ], 103 | ]; 104 | $elements['#attached']['html_head'][] = [$xuacompatible, 'base-href']; 105 | return $elements; 106 | 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function viewElements(FieldItemListInterface $items, $langcode) { 113 | 114 | $entity = $items->getEntity(); 115 | $entity_type = $entity->getEntityTypeId(); 116 | 117 | // The list of OpenAPI specs to pass to SmartDocs Angular app. 118 | $openapi_files = []; 119 | 120 | /** @var \Drupal\file\Entity\File $file */ 121 | foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) { 122 | $openapi_files[] = [ 123 | // Get the URL of the file on server. 124 | 'fileUrl' => $file->createFileUrl(FALSE), 125 | 'fileExtension' => pathinfo($file->getFilename(), PATHINFO_EXTENSION), 126 | ]; 127 | } 128 | 129 | $elements['#attached'] = [ 130 | 'library' => [ 131 | 'apigee_api_catalog/js_yaml', 132 | 'apigee_api_catalog/smartdocs_integration', 133 | 'apigee_api_catalog/smartdocs', 134 | ], 135 | ]; 136 | 137 | $elements['#attached']['drupalSettings']['smartdocsFieldFormatter'][$this->fieldDefinition->getName()] = [ 138 | 'openApiFiles' => $openapi_files, 139 | 'entityId' => $entity->id(), 140 | 'entityType' => $entity_type, 141 | ]; 142 | 143 | foreach ($items as $delta => $item) { 144 | $elements[$delta] = [ 145 | // Create tag on page for SmartDocs Angular app. 146 | '#type' => 'html_tag', 147 | '#tag' => 'app-root', 148 | '#value' => $this->t('Loading...'), 149 | ]; 150 | } 151 | 152 | return $elements; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php: -------------------------------------------------------------------------------- 1 | httpClient = $http_client; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public static function create(ContainerInterface $container) { 57 | return new static($container->get('http_client')); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function validate($items, Constraint $constraint) { 64 | /** @var \Drupal\Core\Field\FieldItemListInterface $items */ 65 | if (!isset($items)) { 66 | return; 67 | } 68 | 69 | foreach ($items as $item) { 70 | if ($item->isEmpty()) { 71 | continue; 72 | } 73 | 74 | // Try to resolve the given URI to a URL. It may fail if it's scheme-less. 75 | try { 76 | $url = Url::fromUri($item->getValue()['uri'], ['absolute' => TRUE])->toString(); 77 | } 78 | catch (\InvalidArgumentException $e) { 79 | $this->context->addViolation($constraint->urlParseError, ['@error' => $e->getMessage()]); 80 | return; 81 | } 82 | 83 | try { 84 | $options = [ 85 | 'allow_redirects' => [ 86 | 'strict' => TRUE, 87 | ], 88 | ]; 89 | 90 | // Perform only a HEAD method to save bandwidth. 91 | /** @var \Psr\Http\Message\ResponseInterface $response */ 92 | $response = $this->httpClient->head($url, $options); 93 | } 94 | catch (RequestException $request_exception) { 95 | $this->context->addViolation($constraint->notValid, [ 96 | '%value' => $url, 97 | ]); 98 | } 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/SpecFetcherInterface.php: -------------------------------------------------------------------------------- 1 | MessengerInterface::TYPE_WARNING, 48 | LogLevel::WARNING => MessengerInterface::TYPE_WARNING, 49 | LogLevel::NOTICE => MessengerInterface::TYPE_STATUS, 50 | LogLevel::INFO => MessengerInterface::TYPE_STATUS, 51 | LogLevel::DEBUG => MessengerInterface::TYPE_STATUS, 52 | ]; 53 | 54 | /** 55 | * The status when an error happened during the fetch operation. 56 | * 57 | * @var string 58 | */ 59 | public const STATUS_ERROR = 'status_error'; 60 | 61 | /** 62 | * The status when a spec update completed successfully. 63 | * 64 | * @var string 65 | */ 66 | public const STATUS_UPDATED = 'status_updated'; 67 | 68 | /** 69 | * The status when a spec update finds the remote file unchanged. 70 | * 71 | * @var string 72 | */ 73 | public const STATUS_UNCHANGED = 'status_unchanged'; 74 | 75 | /** 76 | * Fetch OpenAPI specification file from URL. 77 | * 78 | * Takes care of updating an ApiDoc file entity with the updated spec file. If 79 | * "spec_file_source" uses a URL, it will fetch the specified file and put it 80 | * in the "spec" file field. If it uses a "file", it won't change it. 81 | * This method only updates the file entity if it completed without error (if 82 | * it returns STATUS_UPDATED or STATUS_UNCHANGED), it does not save 83 | * the ApiDoc entity. 84 | * 85 | * @param \Drupal\node\NodeInterface $apidoc 86 | * The ApiDoc entity. 87 | * 88 | * @return string 89 | * Returns the status of the operation. If it is STATUS_UPDATED or 90 | * STATUS_UNCHANGED, the ApiDoc entity will need to be saved to store the 91 | * changes. 92 | */ 93 | public function fetchSpec(NodeInterface $apidoc): string; 94 | 95 | } 96 | -------------------------------------------------------------------------------- /tests/src/Functional/ApiDocsAdminTest.php: -------------------------------------------------------------------------------- 1 | drupalPlaceBlock('local_actions_block'); 72 | $this->drupalPlaceBlock('local_tasks_block'); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | protected function setUp(): void { 79 | parent::setUp(); 80 | 81 | // Add the system menu blocks to appropriate regions. 82 | $this->setupMenus(); 83 | 84 | $this->adminUser = $this->drupalCreateUser([ 85 | 'access content', 86 | 'access content overview', 87 | 'create apidoc content', 88 | 'edit any apidoc content', 89 | 'delete any apidoc content', 90 | ]); 91 | $this->drupalLogin($this->adminUser); 92 | } 93 | 94 | /** 95 | * Tests that a user can administer API Docs. 96 | */ 97 | public function testApiDocAdministration() { 98 | $assert = $this->assertSession(); 99 | 100 | // Get the API Doc admin page. 101 | $this->drupalGet(Url::fromRoute('view.api_catalog_admin.page_1')); 102 | 103 | // No API docs yet. 104 | $assert->pageTextContains('There are no API docs yet.'); 105 | 106 | // User can add entity content. 107 | $assert->linkExists('OpenAPI'); 108 | $this->clickLink('OpenAPI'); 109 | 110 | // Fields should have proper defaults. 111 | $assert->fieldValueEquals('title[0][value]', ''); 112 | $assert->fieldValueEquals('body[0][value]', ''); 113 | 114 | // Create a new spec in site. 115 | $file = File::create([ 116 | 'uid' => $this->adminUser->id(), 117 | 'filename' => 'specA.yaml', 118 | 'uri' => 'public://specA.yaml', 119 | 'filemime' => 'application/octet-stream', 120 | 'created' => 1, 121 | 'changed' => 1, 122 | 'status' => FileInterface::STATUS_PERMANENT, 123 | ]); 124 | file_put_contents($file->getFileUri(), "swagger: '2.0'"); 125 | 126 | // Save it, inserting a new record. 127 | $file->setPermanent(); 128 | $file->save(); 129 | $this->assertTrue($file->id() > 0, 'The file was added to the database.'); 130 | 131 | $page = $this->getSession()->getPage(); 132 | $random_name = $this->randomMachineName(); 133 | $random_description = Random::getGenerator()->sentences(5); 134 | $page->fillField('title[0][value]', $random_name); 135 | $page->fillField('body[0][value]', $random_description); 136 | $page->fillField('field_apidoc_spec_file_source', 'file'); 137 | 138 | // Can't use drupalPostForm() to set hidden fields. 139 | $this->getSession()->getPage()->find( 140 | 'css', 'input[name="field_apidoc_spec[0][fids]"]' 141 | )->setValue($file->id()); 142 | $this->getSession()->getPage()->pressButton(t('Save')); 143 | 144 | $assert->statusCodeEquals(200); 145 | $assert->pageTextContains(new FormattableMarkup( 146 | 'OpenAPI Doc @name has been created.', 147 | [ 148 | '@name' => $random_name, 149 | ] 150 | ) 151 | ); 152 | 153 | // Entity listed. 154 | $assert->linkExists($random_name); 155 | $assert->linkExists('Edit'); 156 | $assert->linkExists('Delete'); 157 | 158 | // Click on API Doc to edit. 159 | $this->clickLink('Edit'); 160 | $assert->statusCodeEquals(200); 161 | 162 | // Edit form should have proper values. 163 | $assert->fieldValueEquals('title[0][value]', $random_name); 164 | $assert->fieldValueEquals('body[0][value]', $random_description); 165 | $assert->linkExists('specA.yaml'); 166 | 167 | // Delete the entity. 168 | $this->clickLink('Delete'); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /tests/src/Functional/ApiDocsBreadcrumbTest.php: -------------------------------------------------------------------------------- 1 | drupalPlaceBlock('system_breadcrumb_block'); 62 | $this->drupalPlaceBlock('page_title_block'); 63 | 64 | $this->apidoc = $this->container->get('entity_type.manager') 65 | ->getStorage('node') 66 | ->create([ 67 | 'type' => 'apidoc', 68 | 'title' => 'API 1', 69 | 'body' => [ 70 | 'value' => 'Test API 1', 71 | 'format' => 'basic_html', 72 | ], 73 | 'field_apidoc_spec' => NULL, 74 | ]); 75 | 76 | $this->apidoc->save(); 77 | 78 | $user = $this->drupalCreateUser(['access content']); 79 | $this->drupalLogin($user); 80 | } 81 | 82 | /** 83 | * Tests the route subscriber will redirect from smartdoc routes. 84 | */ 85 | public function testApiDocBreadcrumb() { 86 | // Tests the normal response. 87 | $url = Url::fromRoute('entity.node.canonical', ['node' => $this->apidoc->id()]); 88 | $this->drupalGet($url); 89 | 90 | // Fetch each node title in the current breadcrumb. 91 | $links = $this->xpath('//nav[@class="breadcrumb"]/div/ol/li/a'); 92 | $got_breadcrumb = []; 93 | foreach ($links as $link) { 94 | $got_breadcrumb[] = $link->getText(); 95 | } 96 | 97 | $this->assertEquals($got_breadcrumb[0], 'Home'); 98 | $this->assertEquals($got_breadcrumb[1], 'API Catalog'); 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /tests/src/Functional/ApiDocsJsonApi.php: -------------------------------------------------------------------------------- 1 | container->get('entity_type.manager')->getStorage('node'); 74 | 75 | // Create published apidoc. 76 | $this->apidocPublished = $nodeStorage->create([ 77 | 'type' => 'apidoc', 78 | 'title' => 'Published', 79 | 'body' => [ 80 | 'value' => 'Published API', 81 | 'format' => 'basic_html', 82 | ], 83 | 'field_apidoc_spec' => NULL, 84 | 'status' => 1, 85 | ]); 86 | $this->apidocPublished->save(); 87 | 88 | // Create unpublished apidoc. 89 | $this->apidocUnpublished = $nodeStorage->create([ 90 | 'type' => 'apidoc', 91 | 'title' => 'Unpublished', 92 | 'body' => [ 93 | 'value' => 'Unpublished API', 94 | 'format' => 'basic_html', 95 | ], 96 | 'field_apidoc_spec' => NULL, 97 | 'status' => 0, 98 | ]); 99 | $this->apidocUnpublished->save(); 100 | } 101 | 102 | /** 103 | * Test listing API Docs as an admin. 104 | * 105 | * Admin should see all API Docs. 106 | */ 107 | public function testListAdminAccess() { 108 | $account = $this->drupalCreateUser([ 109 | 'bypass node access', 110 | ]); 111 | $this->drupalLogin($account); 112 | 113 | $collection_url = Url::fromRoute('jsonapi.node--apidoc.collection') 114 | ->setAbsolute(TRUE)->toString(); 115 | 116 | $this->verifyAccess($account, [ 117 | $this->apidocPublished, 118 | $this->apidocUnpublished, 119 | ], $collection_url); 120 | } 121 | 122 | /** 123 | * Make sure admin can filter and get results back. 124 | * 125 | * @throws \Drupal\Core\Entity\EntityStorageException 126 | */ 127 | public function testFilterAdminAccess() { 128 | $account = $this->drupalCreateUser([ 129 | 'bypass node access', 130 | ]); 131 | $this->drupalLogin($account); 132 | 133 | $collection_url = Url::fromRoute('jsonapi.node--apidoc.collection') 134 | ->setAbsolute(TRUE)->toString(); 135 | $url = "${collection_url}?filter[title]=Published"; 136 | $this->verifyAccess($account, [$this->apidocPublished], $url); 137 | 138 | $url = "${collection_url}?filter[title]=Unpublished"; 139 | $this->verifyAccess($account, [$this->apidocUnpublished], $url); 140 | } 141 | 142 | /** 143 | * View published permission can filter published docs. 144 | * 145 | * @throws \Drupal\Core\Entity\EntityStorageException 146 | */ 147 | public function testFilterViewAccessViewPublished() { 148 | $account = $this->drupalCreateUser([ 149 | 'access content', 150 | ]); 151 | $this->drupalLogin($account); 152 | 153 | $collection_url = Url::fromRoute('jsonapi.node--apidoc.collection') 154 | ->setAbsolute(TRUE)->toString(); 155 | $url = "${collection_url}?filter[title]=Published"; 156 | $this->verifyAccess($account, [$this->apidocPublished], $url); 157 | } 158 | 159 | /** 160 | * Returns Guzzle request options for authentication. 161 | * 162 | * @return array 163 | * Guzzle request options to use for authentication. 164 | * 165 | * @see \GuzzleHttp\ClientInterface::request() 166 | */ 167 | protected function getAuthenticationRequestOptions($account) { 168 | return [ 169 | 'headers' => [ 170 | 'Authorization' => 'Basic ' . base64_encode($account->name->value . ':' . $account->passRaw), 171 | ], 172 | ]; 173 | } 174 | 175 | /** 176 | * Verify the account has access when making JSON:API call. 177 | * 178 | * @param \Drupal\Core\Session\AccountInterface $account 179 | * The account to send the call. 180 | * @param array $apidocs_expected 181 | * An array of the expected API Docs. 182 | * @param string $url 183 | * The URL to call. 184 | * @param array $request_options 185 | * Any request parameters to pass in such as filter query params. 186 | */ 187 | protected function verifyAccess(AccountInterface $account, array $apidocs_expected, string $url, array $request_options = []) { 188 | 189 | // Need this header to make calls. 190 | $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json'; 191 | // Add request options and basic auth header together. 192 | $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions($account)); 193 | 194 | $client = $this->getSession()->getDriver()->getClient()->getClient(); 195 | 196 | // Make the API call. 197 | $response = $client->request('GET', $url, $request_options); 198 | 199 | $this->assertSame(['application/vnd.api+json'], $response->getHeader('Content-Type')); 200 | $response_document = Json::decode((string) $response->getBody()); 201 | 202 | // Get the API Docs from response and create array of names fetched. 203 | $apidocs_response = $response_document['data']; 204 | $names = []; 205 | foreach ($apidocs_response as $apidoc) { 206 | $names[] = $apidoc['attributes']['title']; 207 | } 208 | 209 | // Sort expected and actual response results by name for comparison. 210 | usort($apidocs_expected, function ($a, $b) { 211 | return strcmp($a->label(), $b->label()); 212 | }); 213 | usort($apidocs_response, function ($a, $b) { 214 | return strcmp($a['attributes']['title'], $b['attributes']['title']); 215 | }); 216 | for ($i = 0; $i < count($apidocs_response); $i++) { 217 | $this->assertEquals($apidocs_expected[$i]->label(), $apidocs_response[$i]['attributes']['title']); 218 | } 219 | // Make sure the count is the same. 220 | $this->assertCount(count($apidocs_expected), $apidocs_response, 'Count of API Docs returned does not match count of expected.'); 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /tests/src/Functional/SmartdocRoutingTest.php: -------------------------------------------------------------------------------- 1 | apidoc = $this->container->get('entity_type.manager') 61 | ->getStorage('node') 62 | ->create([ 63 | 'type' => 'apidoc', 64 | 'title' => 'API 1', 65 | 'body' => [ 66 | 'value' => 'Test API 1', 67 | 'format' => 'basic_html', 68 | ], 69 | 'field_apidoc_spec' => NULL, 70 | ]); 71 | 72 | $this->apidoc->save(); 73 | 74 | $user = $this->drupalCreateUser(['access content']); 75 | $this->drupalLogin($user); 76 | } 77 | 78 | /** 79 | * Tests the route subscriber will redirect from smartdoc routes. 80 | */ 81 | public function testNotFoundSubscriber() { 82 | $this->assertEquals($this->apidoc->id(), 1); 83 | 84 | // This needs to run before the alias can be picked up? 85 | $this->apidoc->toUrl()->toString(); 86 | $alias = \Drupal::service('path_alias.manager')->getAliasByPath('/node/1', $this->apidoc->language()->getId()); 87 | $this->assertEquals($alias, '/api/1'); 88 | 89 | $assert = $this->assertSession(); 90 | 91 | // Tests the normal response. 92 | $url = Url::fromRoute('entity.node.canonical', ['node' => $this->apidoc->id()]); 93 | $this->drupalGet($url); 94 | $assert->statusCodeEquals(200); 95 | static::assertEmpty($this->getSession()->getResponseHeader('location')); 96 | 97 | // Test the canonical route uses the /api/* path alias. 98 | $this->assertEquals(parse_url($this->getSession()->getCurrentUrl(), PHP_URL_PATH), '/api/1'); 99 | 100 | // Tests the node alias response. 101 | $this->drupalGet('/api/1'); 102 | $assert->statusCodeEquals(200); 103 | static::assertEmpty($this->getSession()->getResponseHeader('location')); 104 | 105 | // Test that the smartdoc routes redirect to the canonical route. 106 | $url = Url::fromUserInput('/api/1/1/overview')->setAbsolute(); 107 | $response = $this->getHttpClient()->request('GET', $url->toString(), [ 108 | 'allow_redirects' => FALSE, 109 | ]); 110 | $this->assertEquals($response->getStatusCode(), 302); 111 | $this->assertEquals($response->getHeader('location')[0], '/api/1'); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /tests/src/Kernel/ApidocEntityTest.php: -------------------------------------------------------------------------------- 1 | installSchema('user', ['users_data']); 72 | $this->installSchema('node', ['node_access']); 73 | $this->installEntitySchema('user'); 74 | $this->installEntitySchema('node'); 75 | $this->installEntitySchema('node_type'); 76 | $this->installEntitySchema('path_alias'); 77 | $this->installConfig(static::$modules); 78 | 79 | $this->entityTypeManager = $this->container->get('entity_type.manager'); 80 | $this->nodeStorage = $this->entityTypeManager->getStorage('node'); 81 | } 82 | 83 | /** 84 | * Basic CRUD operations on a ApiDoc node entity. 85 | */ 86 | public function testEntity() { 87 | $entity = $this->nodeStorage->create([ 88 | 'type' => 'apidoc', 89 | 'title' => 'API 1', 90 | 'body' => [ 91 | 'value' => 'Test API 1', 92 | 'format' => 'basic_html', 93 | ], 94 | 'field_apidoc_spec' => NULL, 95 | ]); 96 | 97 | $this->assertNotNull($entity); 98 | $this->assertEquals(SAVED_NEW, $entity->save()); 99 | $this->assertEquals(SAVED_UPDATED, $entity->set('title', 'API 1a')->save()); 100 | $entity_id = $entity->id(); 101 | $this->assertNotEmpty($entity_id); 102 | 103 | // Test path alias. 104 | // This needs to run before the alias can be picked up? 105 | $entity->toUrl()->toString(); 106 | $alias = \Drupal::service('path_alias.manager')->getAliasByPath('/node/' . $entity->id(), $entity->language()->getId()); 107 | $this->assertEquals($alias, '/api/' . $entity->id()); 108 | 109 | $entity->delete(); 110 | $this->assertNull($this->nodeStorage->load($entity_id)); 111 | } 112 | 113 | /** 114 | * Test revisioning functionality on an apidocs entity. 115 | */ 116 | public function testRevisions() { 117 | /** @var \Drupal\node\NodeInterface $entity */ 118 | 119 | $description_v1 = 'Test API'; 120 | $entity = $this->nodeStorage->create([ 121 | 'type' => 'apidoc', 122 | 'title' => 'API 1', 123 | 'body' => [ 124 | 'value' => $description_v1, 125 | 'format' => 'basic_html', 126 | ], 127 | 'field_apidoc_spec' => NULL, 128 | ]); 129 | 130 | // Test saving a revision. 131 | $entity->setNewRevision(); 132 | $entity->setRevisionLogMessage('v1'); 133 | $entity->save(); 134 | $v1_id = $entity->getRevisionId(); 135 | $this->assertNotNull($v1_id); 136 | 137 | // Test saving a new revision. 138 | $new_log = 'v2'; 139 | $entity->body = [ 140 | 'value' => 'Test API v2', 141 | 'format' => 'basic_html', 142 | ]; 143 | $entity->setNewRevision(); 144 | $entity->setRevisionLogMessage($new_log); 145 | $entity->save(); 146 | $v2_id = $entity->getRevisionId(); 147 | $this->assertLessThan($v2_id, $v1_id); 148 | 149 | // Test saving without a new revision. 150 | $entity->body = [ 151 | 'value' => 'Test API v3', 152 | 'format' => 'basic_html', 153 | ]; 154 | $entity->save(); 155 | $this->assertEquals($v2_id, $entity->getRevisionId()); 156 | 157 | // Test that the revision log message wasn't overriden. 158 | $this->assertEquals($new_log, $entity->getRevisionLogMessage()); 159 | 160 | // Revert to the first revision. 161 | $entity_v1 = $this->nodeStorage->loadRevision($v1_id); 162 | $entity_v1->setNewRevision(); 163 | $entity_v1->isDefaultRevision(TRUE); 164 | $entity_v1->setRevisionLogMessage('Copy of revision ' . $v1_id); 165 | $entity_v1->save(); 166 | 167 | // Load and check reverted values. 168 | $this->nodeStorage->resetCache(); 169 | $reverted = $this->nodeStorage->load($entity->id()); 170 | $this->assertLessThan($reverted->getRevisionId(), $v1_id); 171 | $this->assertTrue($reverted->isDefaultRevision()); 172 | $this->assertEquals($description_v1, $reverted->body->value); 173 | } 174 | 175 | } 176 | --------------------------------------------------------------------------------