docs-util: support generating OAS in docblock generator (#6338)
## What
This PR adds support for generating OAS in the docblock generator tool.
## How
As OAS are generated in a different manner/location than regular TSDocs, it requires a new type of generator within the tool. As such, the existing docblock generator now only handles files that aren't under the `packages/medusa/src/api` and `packages/medusa/src/api-v2` directories. The new generator handles files under these directories. However, it only considers a node to be an API route if it's a function having two parameters of types `MedusaRequest` and `MedusaResponse` respectively. So, only new API Routes are considered.
The new generator runs the same way as the existing docblock generator with the same method. The generators will detect whether they can run on the file or not and the docblocks/oas are generated based on that. I've also added a `--type` option to the CLI commands of the docblock generator tool to further filter and choose which generator to use.
When the OAS generator finds an API route, it will generate its OAS under the `docs-util/oas-output/operations` directory in a TypeScript file. I chose to generate in TS files rather than YAML files to maintain the functionality of `medusa-oas` without major changes.
Schemas detected in the OAS operation, such as the request and response schemas, are generated as OAS schemas under the `docs-util/oas-output/schemas` directory and referenced in operations and other resources.
The OAS generator also handles updating OAS. When you run the same command on a file/directory and an API route already has OAS associated with it, its information and associated schemas are updated instead of generating new schemas/operations. However, summaries and descriptions aren't updated unless they're not available or their values are the default value SUMMARY.
## API Route Handling
### Request and Response Types
The tool extracts the type of request/response schemas from the type arguments passed to the `MedusaRequest` and `MedusaResponse` respectively. For example:
```ts
export const POST = async (
req: MedusaRequest<{
id: string
}>,
res: MedusaResponse<ResponseType>
) => {
// ...
}
```
If these types aren't provided, the request/response is considered empty.
### Path Parameters
Path parameters are extracted from the file's path name. For example, for `packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts` the `id` path parameter is extracted.
### Query Parameters
The tool extracts the query parameters of an API route based on the type of `request.validatedQuery`. Once we narrow down how we're typing query parameters, we can revisit this implementation.
## Changes to Medusa Oas CLI
I added a `--v2` option to the Medusa OAS CLI to support loading OAS from `docs-util/oas-output` directory rather than the `medusa` package. This will output the OAS in `www/apps/api-reference/specs`, wiping out old OAS. This is only helpful for testing purposes to check how the new OAS looks like in the API reference. It also allows us to slowly start adapting the new OAS.
## Other Notes and Changes
- I've added a GitHub action that creates a PR for generated OAS when Version Packages is merged (similar to regular TSDocs). However, this will only generate the OAS in the `docs-util/oas-output` directory and will not affect the existing OAS in the API reference. Once we're ready to include it those OAS, we can talk about next steps.
- I've moved the base YAML from the `medusa` package to the `docs-util/oas-output/base` directory and changed the `medusa-oas` tool to load them from there.
- I added a `clean:oas` command to the docblock generator CLI tool that removes unused OAS operations, schemas, and tags from `docs-util/oas-output`. The tool also supports updating OAS operations and their associated schemas. However, I didn't add a specific mechanism to update schemas on their own as that's a bit tricky and would require the help of typedoc. I believe with the process of running the tool on the `api-v2` directory whenever there's a new release should be enough to update associated schemas, but if we find that not enough, we can revisit updating schemas individually.
- Because of the `clean:oas` command which makes changes to tags (removing the existing ones, more details on this one later), I've added new base YAML under `docs-util/oas-output/base-v2`. This is used by the tool when generating/cleaning OAS, and the Medusa OAS CLI when the `--v2` option is used.
## Testing
### Prerequisites
To test with request/response types, I recommend minimally modifying `packages/medusa/src/types/routing.ts` to allow type arguments of `MedusaRequest` and `MedusaResponse`:
```ts
import type { NextFunction, Request, Response } from "express"
import type { Customer, User } from "../models"
import type { MedusaContainer } from "./global"
export interface MedusaRequest<T = unknown> extends Request {
user?: (User | Customer) & { customer_id?: string; userId?: string }
scope: MedusaContainer
}
export type MedusaResponse<T = unknown> = Response
export type MedusaNextFunction = NextFunction
export type MedusaRequestHandler = (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => Promise<void> | void
```
You can then add type arguments to the routes in `packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts`. For example:
```ts
import {
deleteCampaignsWorkflow,
updateCampaignsWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { CampaignDTO, IPromotionModuleService } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
interface ResponseType {
campaign: CampaignDTO
}
export const GET = async (
req: MedusaRequest,
res: MedusaResponse<ResponseType>
) => {
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
)
const campaign = await promotionModuleService.retrieveCampaign(
req.params.id,
{
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
}
)
res.status(200).json({ campaign })
}
export const POST = async (
req: MedusaRequest<{
id: string
}>,
res: MedusaResponse<ResponseType>
) => {
const updateCampaigns = updateCampaignsWorkflow(req.scope)
const campaignsData = [
{
id: req.params.id,
...(req.validatedBody || {}),
},
]
const { result, errors } = await updateCampaigns.run({
input: { campaignsData },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ campaign: result[0] })
}
export const DELETE = async (
req: MedusaRequest,
res: MedusaResponse<{
id: string
object: string
deleted: boolean
}>
) => {
const id = req.params.id
const manager = req.scope.resolve("manager")
const deleteCampaigns = deleteCampaignsWorkflow(req.scope)
const { errors } = await deleteCampaigns.run({
input: { ids: [id] },
context: { manager },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
id,
object: "campaign",
deleted: true,
})
}
```
### Generate OAS
- Install dependencies in the `docs-util` directory
- Run the following command in the `docs-util/packages/docblock-generator` directory:
```bash
yarn dev run "../../../packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts"
```
This will generate the OAS operation and schemas and necessary and update the base YAML to include the new tags.
### Generate OAS with Examples
By default, the tool will only generate cURL examples for OAS operations. To generate templated JS Client and (placeholder) Medusa React examples, add the `--generate-examples` option to the command:
```bash
yarn dev run "../../../packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts" --generate-examples
```
> Note: the command will update the existing OAS you generated in the previous test.
### Testing Updates
To test updating OAS, you can try updating request/response types, then running the command, and the associated OAS/schemas will be updated.
### Clean OAS
The `clean:oas` command will remove any unused operation, tags, or schemas. To test it out you can try:
- Remove an API Route => this removes its associated operation and schemas (if not referenced anywhere else).
- Remove all references to a schema => this removes the schema.
- Remove all operations in `docs-util/oas-output/operations` associated with a tag => this removes the tag from the base YAML.
```bash
yarn dev clean:oas
```
> Note: when running this command, existing tags in the base YAML (such as Products) will be removed since there are no operations using it. As it's running on the base YAML under `base-v2`, this doesn't affect base YAML used for the API reference.
### Medusa Oas CLI
- Install and build dependencies in the root of the monorepo
- Run the following command to generate reference OAS for v2 API Routes (must have generated OAS previously using the docblock generator tool):
```bash
yarn openapi:generate --v2
```
- This wipes out existing OAS in `www/apps/api-reference/specs` and replaces them with the new ones. At this point, you can view the new API routes in the API reference by running the `yarn dev` command in `www/apps/api-reference` (although not necessary for testing here).
- Run the command again without the `--v2` option:
```bash
yarn openapi:generate
```
The specs in `www/apps/api-reference/specs` are reverted back to the old routes.
This commit is contained in:
7
.changeset/old-boats-fold.md
Normal file
7
.changeset/old-boats-fold.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/medusa-oas-cli": patch
|
||||
"@medusajs/oas-github-ci": patch
|
||||
---
|
||||
|
||||
feat(@medusajs/medusa-oas-cli): added v2 flag
|
||||
feat(@medusajs/oas-github-ci): added v2 flag
|
||||
60
.github/workflows/generate-docblocks.yml
vendored
60
.github/workflows/generate-docblocks.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Run docblock generator
|
||||
if: steps.check-commit.outputs.is_release_commit == true
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
run: "yarn start run:release"
|
||||
working-directory: docs-util/packages/docblock-generator
|
||||
env:
|
||||
@@ -49,13 +49,65 @@ jobs:
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-commit.outputs.is_release_commit == true
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
title: "Generated TSDocs"
|
||||
title: "chore(docs): Generated TSDocs"
|
||||
body: "This PR holds all generated TSDocs for the upcoming release."
|
||||
branch: "chore/generate-tsdocs"
|
||||
team-reviewers: "@medusajs/docs"
|
||||
add-paths: packages/**
|
||||
add-paths: packages/**
|
||||
generate-oas:
|
||||
name: Generated OAS PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Install docs-util Dependencies
|
||||
run: yarn
|
||||
working-directory: docs-util
|
||||
|
||||
- name: Build packages
|
||||
run: yarn build
|
||||
working-directory: docs-util
|
||||
|
||||
- name: Check Commit
|
||||
id: check-commit
|
||||
run: 'yarn check:release-commit ${{ github.sha }}'
|
||||
working-directory: docs-util/packages/scripts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_OWNER: ${{ github.repository_owner }}
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Run docblock generator
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
run: "yarn start run ../../../packages/medusa/src/api-v2 --type oas && yarn start clean:oas"
|
||||
working-directory: docs-util/packages/docblock-generator
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_OWNER: ${{ github.repository_owner }}
|
||||
GIT_REPO: medusa
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-commit.outputs.is_release_commit == 'true'
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
title: "chore(docs): Generated OAS"
|
||||
body: "This PR holds all generated OAS for the upcoming release."
|
||||
branch: "chore/generate-oas"
|
||||
team-reviewers: "@medusajs/docs"
|
||||
add-paths: docs-util/oas-output/**
|
||||
@@ -72,6 +72,7 @@ module.exports = {
|
||||
],
|
||||
"space-infix-ops": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
"no-case-declarations": "off"
|
||||
},
|
||||
env: {
|
||||
es6: true,
|
||||
@@ -79,7 +80,8 @@ module.exports = {
|
||||
},
|
||||
ignorePatterns: [
|
||||
".eslintrc.js",
|
||||
"dist"
|
||||
"dist",
|
||||
"oas-output"
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -7,235 +7,313 @@ info:
|
||||
url: https://github.com/medusajs/medusa/blob/master/LICENSE
|
||||
tags:
|
||||
- name: Apps Oauth
|
||||
description: |
|
||||
Some plugins may require to authenticate with third-party services and store authentication details, such as the authentication token. To do that, they can create an Oauth provider within the plugin that handles the authentication.
|
||||
description: >
|
||||
Some plugins may require to authenticate with third-party services and
|
||||
store authentication details, such as the authentication token. To do
|
||||
that, they can create an Oauth provider within the plugin that handles the
|
||||
authentication.
|
||||
|
||||
The Apps Oauth API Routes allows admins to manage and generate token for an app using its oauth provider.
|
||||
- name: Auth
|
||||
description: |
|
||||
Authentication API Routes allow admin users to manage their session, such as login or log out.
|
||||
description: >
|
||||
Authentication API Routes allow admin users to manage their session, such
|
||||
as login or log out.
|
||||
|
||||
When an admin user is logged in, the cookie header is set indicating the admin's login session.
|
||||
externalDocs:
|
||||
description: How to implement user profiles
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-profile
|
||||
- name: Batch Jobs
|
||||
description: |
|
||||
A batch job is a task that is performed by the Medusa backend asynchronusly. For example, the Import Product feature is implemented using batch jobs.
|
||||
description: >
|
||||
A batch job is a task that is performed by the Medusa backend
|
||||
asynchronusly. For example, the Import Product feature is implemented
|
||||
using batch jobs.
|
||||
|
||||
Batch Job API Routes allow admins to manage the batch jobs and their state.
|
||||
externalDocs:
|
||||
description: How to import products
|
||||
url: https://docs.medusajs.com/modules/products/admin/import-products
|
||||
- name: Currencies
|
||||
description: |
|
||||
A store can use unlimited currencies, and each region must be associated with at least one currency.
|
||||
description: >
|
||||
A store can use unlimited currencies, and each region must be associated
|
||||
with at least one currency.
|
||||
|
||||
Currencies are defined within the Medusa backend. Currency API Routes allow admins to list and update currencies.
|
||||
externalDocs:
|
||||
description: How to manage currencies
|
||||
url: https://docs.medusajs.com/modules/regions-and-currencies/admin/manage-currencies
|
||||
- name: Customers
|
||||
description: |
|
||||
Customers can either be created when they register through the Store APIs, or created by the admin using the Admin APIs.
|
||||
externalDocs:
|
||||
description: How to manage customers
|
||||
url: https://docs.medusajs.com/modules/customers/admin/manage-customers
|
||||
- name: Customer Groups
|
||||
description: |
|
||||
Customer Groups can be used to organize customers that share similar data or attributes into dedicated groups.
|
||||
description: >
|
||||
Customer Groups can be used to organize customers that share similar data
|
||||
or attributes into dedicated groups.
|
||||
|
||||
This can be useful for different purposes such as setting a different price for a specific customer group.
|
||||
externalDocs:
|
||||
description: How to manage customer groups
|
||||
url: https://docs.medusajs.com/modules/customers/admin/manage-customer-groups
|
||||
- name: Customers
|
||||
description: >
|
||||
Customers can either be created when they register through the Store APIs,
|
||||
or created by the admin using the Admin APIs.
|
||||
externalDocs:
|
||||
description: How to manage customers
|
||||
url: https://docs.medusajs.com/modules/customers/admin/manage-customers
|
||||
- name: Discounts
|
||||
description: |
|
||||
Admins can create discounts with conditions and rules, providing them with advanced settings for variety of cases.
|
||||
description: >
|
||||
Admins can create discounts with conditions and rules, providing them with
|
||||
advanced settings for variety of cases.
|
||||
|
||||
The Discount API Routes can be used to manage discounts, their conditions, resources, and more.
|
||||
externalDocs:
|
||||
description: How to manage discounts
|
||||
url: https://docs.medusajs.com/modules/discounts/admin/manage-discounts
|
||||
- name: Draft Orders
|
||||
description: |
|
||||
A draft order is an order created manually by the admin. It allows admins to create orders without direct involvement from the customer.
|
||||
description: >
|
||||
A draft order is an order created manually by the admin. It allows admins
|
||||
to create orders without direct involvement from the customer.
|
||||
externalDocs:
|
||||
description: How to manage draft orders
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-draft-orders
|
||||
- name: Gift Cards
|
||||
description: |
|
||||
Admins can create gift cards and send them directly to customers, specifying options like their balance, region, and more.
|
||||
description: >
|
||||
Admins can create gift cards and send them directly to customers,
|
||||
specifying options like their balance, region, and more.
|
||||
|
||||
These gift cards are different than the saleable gift cards in a store, which are created and managed through Product API Routes.
|
||||
externalDocs:
|
||||
description: How to manage gift cards
|
||||
url: https://docs.medusajs.com/modules/gift-cards/admin/manage-gift-cards#manage-custom-gift-cards
|
||||
- name: Inventory Items
|
||||
description: |
|
||||
Inventory items, provided by the [Inventory Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module), can be used to manage the inventory of saleable items in your store.
|
||||
description: >
|
||||
Inventory items, provided by the [Inventory
|
||||
Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module),
|
||||
can be used to manage the inventory of saleable items in your store.
|
||||
externalDocs:
|
||||
description: How to manage inventory items
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-inventory-items
|
||||
- name: Invites
|
||||
description: |
|
||||
An admin can invite new users to manage their team. This would allow new users to authenticate as admins and perform admin functionalities.
|
||||
description: >
|
||||
An admin can invite new users to manage their team. This would allow new
|
||||
users to authenticate as admins and perform admin functionalities.
|
||||
externalDocs:
|
||||
description: How to manage invites
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-invites
|
||||
- name: Notes
|
||||
description: |
|
||||
Notes are created by admins and can be associated with any resource. For example, an admin can add a note to an order for additional details or remarks.
|
||||
description: >
|
||||
Notes are created by admins and can be associated with any resource. For
|
||||
example, an admin can add a note to an order for additional details or
|
||||
remarks.
|
||||
- name: Notifications
|
||||
description: |
|
||||
Notifications are sent to customers to inform them of new updates. For example, a notification can be sent to the customer when their order is place or its state is updated.
|
||||
description: >
|
||||
Notifications are sent to customers to inform them of new updates. For
|
||||
example, a notification can be sent to the customer when their order is
|
||||
place or its state is updated.
|
||||
|
||||
The notification's type, such as an email or SMS, is determined by the notification provider installed on the Medusa backend.
|
||||
- name: Order Edits
|
||||
description: >
|
||||
An admin can edit an order to remove, add, or update an item's quantity.
|
||||
When an admin edits an order, they're stored as an `OrderEdit`.
|
||||
externalDocs:
|
||||
description: How to edit an order
|
||||
url: https://docs.medusajs.com/modules/orders/admin/edit-order
|
||||
- name: Orders
|
||||
description: |
|
||||
Orders are purchases made by customers, typically through a storefront using the Store API. Draft orders created by the admin are also transformed to an Order once the payment is captured.
|
||||
description: >
|
||||
Orders are purchases made by customers, typically through a storefront
|
||||
using the Store API. Draft orders created by the admin are also
|
||||
transformed to an Order once the payment is captured.
|
||||
|
||||
Managing orders include managing fulfillment, payment, claims, reservations, and more.
|
||||
externalDocs:
|
||||
description: How to manage orders
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-orders
|
||||
- name: Order Edits
|
||||
description: |
|
||||
An admin can edit an order to remove, add, or update an item's quantity. When an admin edits an order, they're stored as an `OrderEdit`.
|
||||
externalDocs:
|
||||
description: How to edit an order
|
||||
url: https://docs.medusajs.com/modules/orders/admin/edit-order
|
||||
- name: Payments
|
||||
description: |
|
||||
A payment can be related to an order, swap, return, or more. It can be captured or refunded.
|
||||
- name: Payment Collections
|
||||
description: |
|
||||
A payment collection is useful for managing additional payments, such as for Order Edits, or installment payments.
|
||||
description: >
|
||||
A payment collection is useful for managing additional payments, such as
|
||||
for Order Edits, or installment payments.
|
||||
- name: Payments
|
||||
description: >
|
||||
A payment can be related to an order, swap, return, or more. It can be
|
||||
captured or refunded.
|
||||
- name: Price Lists
|
||||
description: |
|
||||
A price list are special prices applied to products based on a set of conditions, such as customer group.
|
||||
description: >
|
||||
A price list are special prices applied to products based on a set of
|
||||
conditions, such as customer group.
|
||||
externalDocs:
|
||||
description: How to manage price lists
|
||||
url: https://docs.medusajs.com/modules/price-lists/admin/manage-price-lists
|
||||
- name: Products
|
||||
description: |
|
||||
Products are saleable items in a store. This also includes [saleable gift cards](https://docs.medusajs.com/modules/gift-cards/admin/manage-gift-cards#manage-gift-card-product) in a store.
|
||||
externalDocs:
|
||||
description: How to manage products
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-products
|
||||
- name: Product Categories
|
||||
description: |
|
||||
Products can be categoriezed into categories. A product can be added into more than one category.
|
||||
description: >
|
||||
Products can be categoriezed into categories. A product can be added into
|
||||
more than one category.
|
||||
externalDocs:
|
||||
description: How to manage product categories
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-categories
|
||||
- name: Product Collections
|
||||
description: |
|
||||
A product collection is used to organize products for different purposes such as marketing or discount purposes. For example, you can create a Summer Collection.
|
||||
description: >
|
||||
A product collection is used to organize products for different purposes
|
||||
such as marketing or discount purposes. For example, you can create a
|
||||
Summer Collection.
|
||||
- name: Product Tags
|
||||
description: |
|
||||
Product tags are string values created when you create or update a product with a new tag.
|
||||
description: >
|
||||
Product tags are string values created when you create or update a product
|
||||
with a new tag.
|
||||
|
||||
Products can have more than one tag, and products can share tags. This allows admins to associate products to similar tags that can be used to filter products.
|
||||
- name: Product Types
|
||||
description: |
|
||||
Product types are string values created when you create or update a product with a new type.
|
||||
description: >
|
||||
Product types are string values created when you create or update a
|
||||
product with a new type.
|
||||
|
||||
Products can have one type, and products can share types. This allows admins to associate products with a type that can be used to filter products.
|
||||
- name: Product Variants
|
||||
description: |
|
||||
Product variants are the actual salable item in your store. Each variant is a combination of the different option values available on the product.
|
||||
description: >
|
||||
Product variants are the actual salable item in your store. Each variant
|
||||
is a combination of the different option values available on the product.
|
||||
|
||||
Product variants can be managed through the Products API Routes.
|
||||
externalDocs:
|
||||
description: How to manage product variants
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-products#manage-product-variants
|
||||
- name: Products
|
||||
description: >
|
||||
Products are saleable items in a store. This also includes [saleable gift
|
||||
cards](https://docs.medusajs.com/modules/gift-cards/admin/manage-gift-cards#manage-gift-card-product)
|
||||
in a store.
|
||||
externalDocs:
|
||||
description: How to manage products
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-products
|
||||
- name: Publishable API Keys
|
||||
description: |
|
||||
Publishable API Keys can be used to scope Store API calls with an API key, determining what resources are retrieved when querying the API.
|
||||
description: >
|
||||
Publishable API Keys can be used to scope Store API calls with an API key,
|
||||
determining what resources are retrieved when querying the API.
|
||||
|
||||
For example, a publishable API key can be associated with one or more sales channels. When it is passed in the header of a request to the List Product store API Route,
|
||||
|
||||
the sales channels are inferred from the key and only products associated with those sales channels are retrieved.
|
||||
|
||||
Admins can manage publishable API keys and their associated resources. Currently, only Sales Channels are supported as a resource.
|
||||
externalDocs:
|
||||
description: How to manage publishable API keys
|
||||
url: https://docs.medusajs.com/development/publishable-api-keys/admin/manage-publishable-api-keys
|
||||
- name: Reservations
|
||||
description: |
|
||||
Reservations, provided by the [Inventory Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module), are quantities of an item that are reserved, typically when an order is placed but not yet fulfilled.
|
||||
Reservations can be associated with any resources, but commonly with line items of an order.
|
||||
externalDocs:
|
||||
description: How to manage item allocations in orders
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-item-allocations-in-orders
|
||||
- name: Regions
|
||||
description: |
|
||||
Regions are different countries or geographical regions that the commerce store serves customers in.
|
||||
description: >
|
||||
Regions are different countries or geographical regions that the commerce
|
||||
store serves customers in.
|
||||
|
||||
Admins can manage these regions, their providers, and more.
|
||||
externalDocs:
|
||||
description: How to manage regions
|
||||
url: https://docs.medusajs.com/modules/regions-and-currencies/admin/manage-regions
|
||||
- name: Reservations
|
||||
description: >
|
||||
Reservations, provided by the [Inventory
|
||||
Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module),
|
||||
are quantities of an item that are reserved, typically when an order is
|
||||
placed but not yet fulfilled.
|
||||
|
||||
Reservations can be associated with any resources, but commonly with line items of an order.
|
||||
externalDocs:
|
||||
description: How to manage item allocations in orders
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-item-allocations-in-orders
|
||||
- name: Return Reasons
|
||||
description: |
|
||||
Return reasons are key-value pairs that are used to specify why an order return is being created.
|
||||
description: >
|
||||
Return reasons are key-value pairs that are used to specify why an order
|
||||
return is being created.
|
||||
|
||||
Admins can manage available return reasons, and they can be used by both admins and customers when creating a return.
|
||||
externalDocs:
|
||||
description: How to manage return reasons
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-returns#manage-return-reasons
|
||||
- name: Returns
|
||||
description: |
|
||||
A return can be created by a customer or an admin to return items in an order.
|
||||
description: >
|
||||
A return can be created by a customer or an admin to return items in an
|
||||
order.
|
||||
|
||||
Admins can manage these returns and change their state.
|
||||
externalDocs:
|
||||
description: How to manage returns
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-returns
|
||||
- name: Sales Channels
|
||||
description: |
|
||||
A sales channel indicates a channel where products can be sold in. For example, a webshop or a mobile app.
|
||||
description: >
|
||||
A sales channel indicates a channel where products can be sold in. For
|
||||
example, a webshop or a mobile app.
|
||||
|
||||
Admins can manage sales channels and the products available in them.
|
||||
externalDocs:
|
||||
description: How to manage sales channels
|
||||
url: https://docs.medusajs.com/modules/sales-channels/admin/manage
|
||||
- name: Shipping Options
|
||||
description: |
|
||||
A shipping option is used to define the available shipping methods during checkout or when creating a return.
|
||||
description: >
|
||||
A shipping option is used to define the available shipping methods during
|
||||
checkout or when creating a return.
|
||||
|
||||
Admins can create an unlimited number of shipping options, each associated with a shipping profile and fulfillment provider, among other resources.
|
||||
externalDocs:
|
||||
description: Shipping Option architecture
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-option
|
||||
- name: Shipping Profiles
|
||||
description: |
|
||||
A shipping profile is used to group products that can be shipped in the same manner.
|
||||
description: >
|
||||
A shipping profile is used to group products that can be shipped in the
|
||||
same manner.
|
||||
|
||||
They are created by the admin and they're not associated with a fulfillment provider.
|
||||
externalDocs:
|
||||
description: Shipping Profile architecture
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-profile
|
||||
- name: Stock Locations
|
||||
description: |
|
||||
A stock location, provided by the [Stock Location module](https://docs.medusajs.com/modules/multiwarehouse/stock-location-module), indicates a physical address that stock-kept items, such as physical products, can be stored in.
|
||||
description: >
|
||||
A stock location, provided by the [Stock Location
|
||||
module](https://docs.medusajs.com/modules/multiwarehouse/stock-location-module),
|
||||
indicates a physical address that stock-kept items, such as physical
|
||||
products, can be stored in.
|
||||
|
||||
An admin can create and manage available stock locations.
|
||||
externalDocs:
|
||||
description: How to manage stock locations.
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-stock-locations
|
||||
- name: Store
|
||||
description: |
|
||||
A store indicates the general configurations and details about the commerce store. By default, there's only one store in the Medusa backend.
|
||||
description: >
|
||||
A store indicates the general configurations and details about the
|
||||
commerce store. By default, there's only one store in the Medusa backend.
|
||||
|
||||
Admins can manage the store and its details or configurations.
|
||||
- name: Swaps
|
||||
description: |
|
||||
A swap is created by a customer or an admin to exchange an item with a new one.
|
||||
description: >
|
||||
A swap is created by a customer or an admin to exchange an item with a new
|
||||
one.
|
||||
|
||||
Creating a swap implicitely includes creating a return for the item being exchanged.
|
||||
externalDocs:
|
||||
description: How to manage swaps
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-swaps
|
||||
- name: Tax Rates
|
||||
description: |
|
||||
Each region has at least a default tax rate. Admins can create and manage additional tax rates that can be applied for certain conditions, such as for specific product types.
|
||||
description: >
|
||||
Each region has at least a default tax rate. Admins can create and manage
|
||||
additional tax rates that can be applied for certain conditions, such as
|
||||
for specific product types.
|
||||
externalDocs:
|
||||
description: How to manage tax rates
|
||||
url: https://docs.medusajs.com/modules/taxes/admin/manage-tax-rates
|
||||
- name: Uploads
|
||||
description: |
|
||||
The upload API Routes are used to upload any type of resources. For example, they can be used to upload CSV files that are used to import products into the store.
|
||||
description: >
|
||||
The upload API Routes are used to upload any type of resources. For
|
||||
example, they can be used to upload CSV files that are used to import
|
||||
products into the store.
|
||||
externalDocs:
|
||||
description: How to upload CSV file when importing a product.
|
||||
url: https://docs.medusajs.com/modules/products/admin/import-products#1-upload-csv-file
|
||||
- name: Users
|
||||
description: |
|
||||
A store can have more than one user, each having the same privileges. Admins can manage users, their passwords, and more.
|
||||
description: >
|
||||
A store can have more than one user, each having the same privileges.
|
||||
Admins can manage users, their passwords, and more.
|
||||
externalDocs:
|
||||
description: How to manage users
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-users
|
||||
servers:
|
||||
- url: http://localhost:9000
|
||||
- url: https://api.medusa-commerce.com
|
||||
paths: { }
|
||||
paths: {}
|
||||
components:
|
||||
responses:
|
||||
default_error:
|
||||
@@ -245,9 +323,9 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "unknown_error"
|
||||
message: "An unknown error occurred."
|
||||
type: "unknown_error"
|
||||
code: unknown_error
|
||||
message: An unknown error occurred.
|
||||
type: unknown_error
|
||||
invalid_state_error:
|
||||
description: Invalid State Error
|
||||
content:
|
||||
@@ -255,9 +333,10 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "unknown_error"
|
||||
message: "The request conflicted with another request. You may retry the request with the provided Idempotency-Key."
|
||||
type: "QueryRunnerAlreadyReleasedError"
|
||||
code: unknown_error
|
||||
message: The request conflicted with another request. You may retry the request
|
||||
with the provided Idempotency-Key.
|
||||
type: QueryRunnerAlreadyReleasedError
|
||||
invalid_request_error:
|
||||
description: Invalid Request Error
|
||||
content:
|
||||
@@ -265,9 +344,9 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "invalid_request_error"
|
||||
message: "Discount with code TEST already exists."
|
||||
type: "duplicate_error"
|
||||
code: invalid_request_error
|
||||
message: Discount with code TEST already exists.
|
||||
type: duplicate_error
|
||||
not_found_error:
|
||||
description: Not Found Error
|
||||
content:
|
||||
@@ -275,8 +354,8 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
message: "Entity with id 1 was not found"
|
||||
type: "not_found"
|
||||
message: Entity with id 1 was not found
|
||||
type: not_found
|
||||
400_error:
|
||||
description: Client Error or Multiple Errors
|
||||
content:
|
||||
@@ -308,7 +387,7 @@ components:
|
||||
default_error:
|
||||
$ref: "#/components/examples/default_error"
|
||||
unauthorized:
|
||||
description: 'User is not authorized. Must log in first'
|
||||
description: User is not authorized. Must log in first
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
@@ -316,7 +395,7 @@ components:
|
||||
default: Unauthorized
|
||||
example: Unauthorized
|
||||
incorrect_credentials:
|
||||
description: 'User does not exist or incorrect credentials'
|
||||
description: User does not exist or incorrect credentials
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
@@ -327,44 +406,45 @@ components:
|
||||
not_allowed_error:
|
||||
summary: Not Allowed Error
|
||||
value:
|
||||
message: "Discount must be set to dynamic"
|
||||
type: "not_allowed"
|
||||
message: Discount must be set to dynamic
|
||||
type: not_allowed
|
||||
invalid_data_error:
|
||||
summary: Invalid Data Error
|
||||
value:
|
||||
message: "first_name must be a string"
|
||||
type: "invalid_data"
|
||||
message: first_name must be a string
|
||||
type: invalid_data
|
||||
multiple_errors:
|
||||
summary: Multiple Errors
|
||||
value:
|
||||
message: "Provided request body contains errors. Please check the data and retry the request"
|
||||
message: Provided request body contains errors. Please check the data and retry
|
||||
the request
|
||||
errors:
|
||||
- message: "first_name must be a string"
|
||||
type: "invalid_data"
|
||||
- message: "Discount must be set to dynamic"
|
||||
type: "not_allowed"
|
||||
- message: first_name must be a string
|
||||
type: invalid_data
|
||||
- message: Discount must be set to dynamic
|
||||
type: not_allowed
|
||||
database_error:
|
||||
summary: Database Error
|
||||
value:
|
||||
code: "api_error"
|
||||
message: "An error occured while hashing password"
|
||||
type: "database_error"
|
||||
code: api_error
|
||||
message: An error occured while hashing password
|
||||
type: database_error
|
||||
unexpected_state_error:
|
||||
summary: Unexpected State Error
|
||||
value:
|
||||
message: "cart.total must be defined"
|
||||
type: "unexpected_state"
|
||||
message: cart.total must be defined
|
||||
type: unexpected_state
|
||||
invalid_argument_error:
|
||||
summary: Invalid Argument Error
|
||||
value:
|
||||
message: "cart.total must be defined"
|
||||
type: "unexpected_state"
|
||||
message: cart.total must be defined
|
||||
type: unexpected_state
|
||||
default_error:
|
||||
summary: Default Error
|
||||
value:
|
||||
code: "unknown_error"
|
||||
message: "An unknown error occurred."
|
||||
type: "unknown_error"
|
||||
code: unknown_error
|
||||
message: An unknown error occurred.
|
||||
type: unknown_error
|
||||
securitySchemes:
|
||||
api_token:
|
||||
type: apiKey
|
||||
@@ -379,4 +459,4 @@ components:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: connect.sid
|
||||
x-displayName: Cookie Session ID
|
||||
x-displayName: Cookie Session ID
|
||||
462
docs-util/oas-output/base/admin.oas.base.yaml
Normal file
462
docs-util/oas-output/base/admin.oas.base.yaml
Normal file
@@ -0,0 +1,462 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Medusa Admin API
|
||||
license:
|
||||
name: MIT
|
||||
url: https://github.com/medusajs/medusa/blob/master/LICENSE
|
||||
tags:
|
||||
- name: Apps Oauth
|
||||
description: >
|
||||
Some plugins may require to authenticate with third-party services and
|
||||
store authentication details, such as the authentication token. To do
|
||||
that, they can create an Oauth provider within the plugin that handles the
|
||||
authentication.
|
||||
|
||||
The Apps Oauth API Routes allows admins to manage and generate token for an app using its oauth provider.
|
||||
- name: Auth
|
||||
description: >
|
||||
Authentication API Routes allow admin users to manage their session, such
|
||||
as login or log out.
|
||||
|
||||
When an admin user is logged in, the cookie header is set indicating the admin's login session.
|
||||
externalDocs:
|
||||
description: How to implement user profiles
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-profile
|
||||
- name: Batch Jobs
|
||||
description: >
|
||||
A batch job is a task that is performed by the Medusa backend
|
||||
asynchronusly. For example, the Import Product feature is implemented
|
||||
using batch jobs.
|
||||
|
||||
Batch Job API Routes allow admins to manage the batch jobs and their state.
|
||||
externalDocs:
|
||||
description: How to import products
|
||||
url: https://docs.medusajs.com/modules/products/admin/import-products
|
||||
- name: Currencies
|
||||
description: >
|
||||
A store can use unlimited currencies, and each region must be associated
|
||||
with at least one currency.
|
||||
|
||||
Currencies are defined within the Medusa backend. Currency API Routes allow admins to list and update currencies.
|
||||
externalDocs:
|
||||
description: How to manage currencies
|
||||
url: https://docs.medusajs.com/modules/regions-and-currencies/admin/manage-currencies
|
||||
- name: Customer Groups
|
||||
description: >
|
||||
Customer Groups can be used to organize customers that share similar data
|
||||
or attributes into dedicated groups.
|
||||
|
||||
This can be useful for different purposes such as setting a different price for a specific customer group.
|
||||
externalDocs:
|
||||
description: How to manage customer groups
|
||||
url: https://docs.medusajs.com/modules/customers/admin/manage-customer-groups
|
||||
- name: Customers
|
||||
description: >
|
||||
Customers can either be created when they register through the Store APIs,
|
||||
or created by the admin using the Admin APIs.
|
||||
externalDocs:
|
||||
description: How to manage customers
|
||||
url: https://docs.medusajs.com/modules/customers/admin/manage-customers
|
||||
- name: Discounts
|
||||
description: >
|
||||
Admins can create discounts with conditions and rules, providing them with
|
||||
advanced settings for variety of cases.
|
||||
|
||||
The Discount API Routes can be used to manage discounts, their conditions, resources, and more.
|
||||
externalDocs:
|
||||
description: How to manage discounts
|
||||
url: https://docs.medusajs.com/modules/discounts/admin/manage-discounts
|
||||
- name: Draft Orders
|
||||
description: >
|
||||
A draft order is an order created manually by the admin. It allows admins
|
||||
to create orders without direct involvement from the customer.
|
||||
externalDocs:
|
||||
description: How to manage draft orders
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-draft-orders
|
||||
- name: Gift Cards
|
||||
description: >
|
||||
Admins can create gift cards and send them directly to customers,
|
||||
specifying options like their balance, region, and more.
|
||||
|
||||
These gift cards are different than the saleable gift cards in a store, which are created and managed through Product API Routes.
|
||||
externalDocs:
|
||||
description: How to manage gift cards
|
||||
url: https://docs.medusajs.com/modules/gift-cards/admin/manage-gift-cards#manage-custom-gift-cards
|
||||
- name: Inventory Items
|
||||
description: >
|
||||
Inventory items, provided by the [Inventory
|
||||
Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module),
|
||||
can be used to manage the inventory of saleable items in your store.
|
||||
externalDocs:
|
||||
description: How to manage inventory items
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-inventory-items
|
||||
- name: Invites
|
||||
description: >
|
||||
An admin can invite new users to manage their team. This would allow new
|
||||
users to authenticate as admins and perform admin functionalities.
|
||||
externalDocs:
|
||||
description: How to manage invites
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-invites
|
||||
- name: Notes
|
||||
description: >
|
||||
Notes are created by admins and can be associated with any resource. For
|
||||
example, an admin can add a note to an order for additional details or
|
||||
remarks.
|
||||
- name: Notifications
|
||||
description: >
|
||||
Notifications are sent to customers to inform them of new updates. For
|
||||
example, a notification can be sent to the customer when their order is
|
||||
place or its state is updated.
|
||||
|
||||
The notification's type, such as an email or SMS, is determined by the notification provider installed on the Medusa backend.
|
||||
- name: Order Edits
|
||||
description: >
|
||||
An admin can edit an order to remove, add, or update an item's quantity.
|
||||
When an admin edits an order, they're stored as an `OrderEdit`.
|
||||
externalDocs:
|
||||
description: How to edit an order
|
||||
url: https://docs.medusajs.com/modules/orders/admin/edit-order
|
||||
- name: Orders
|
||||
description: >
|
||||
Orders are purchases made by customers, typically through a storefront
|
||||
using the Store API. Draft orders created by the admin are also
|
||||
transformed to an Order once the payment is captured.
|
||||
|
||||
Managing orders include managing fulfillment, payment, claims, reservations, and more.
|
||||
externalDocs:
|
||||
description: How to manage orders
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-orders
|
||||
- name: Payment Collections
|
||||
description: >
|
||||
A payment collection is useful for managing additional payments, such as
|
||||
for Order Edits, or installment payments.
|
||||
- name: Payments
|
||||
description: >
|
||||
A payment can be related to an order, swap, return, or more. It can be
|
||||
captured or refunded.
|
||||
- name: Price Lists
|
||||
description: >
|
||||
A price list are special prices applied to products based on a set of
|
||||
conditions, such as customer group.
|
||||
externalDocs:
|
||||
description: How to manage price lists
|
||||
url: https://docs.medusajs.com/modules/price-lists/admin/manage-price-lists
|
||||
- name: Product Categories
|
||||
description: >
|
||||
Products can be categoriezed into categories. A product can be added into
|
||||
more than one category.
|
||||
externalDocs:
|
||||
description: How to manage product categories
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-categories
|
||||
- name: Product Collections
|
||||
description: >
|
||||
A product collection is used to organize products for different purposes
|
||||
such as marketing or discount purposes. For example, you can create a
|
||||
Summer Collection.
|
||||
- name: Product Tags
|
||||
description: >
|
||||
Product tags are string values created when you create or update a product
|
||||
with a new tag.
|
||||
|
||||
Products can have more than one tag, and products can share tags. This allows admins to associate products to similar tags that can be used to filter products.
|
||||
- name: Product Types
|
||||
description: >
|
||||
Product types are string values created when you create or update a
|
||||
product with a new type.
|
||||
|
||||
Products can have one type, and products can share types. This allows admins to associate products with a type that can be used to filter products.
|
||||
- name: Product Variants
|
||||
description: >
|
||||
Product variants are the actual salable item in your store. Each variant
|
||||
is a combination of the different option values available on the product.
|
||||
|
||||
Product variants can be managed through the Products API Routes.
|
||||
externalDocs:
|
||||
description: How to manage product variants
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-products#manage-product-variants
|
||||
- name: Products
|
||||
description: >
|
||||
Products are saleable items in a store. This also includes [saleable gift
|
||||
cards](https://docs.medusajs.com/modules/gift-cards/admin/manage-gift-cards#manage-gift-card-product)
|
||||
in a store.
|
||||
externalDocs:
|
||||
description: How to manage products
|
||||
url: https://docs.medusajs.com/modules/products/admin/manage-products
|
||||
- name: Publishable API Keys
|
||||
description: >
|
||||
Publishable API Keys can be used to scope Store API calls with an API key,
|
||||
determining what resources are retrieved when querying the API.
|
||||
|
||||
For example, a publishable API key can be associated with one or more sales channels. When it is passed in the header of a request to the List Product store API Route,
|
||||
|
||||
the sales channels are inferred from the key and only products associated with those sales channels are retrieved.
|
||||
|
||||
Admins can manage publishable API keys and their associated resources. Currently, only Sales Channels are supported as a resource.
|
||||
externalDocs:
|
||||
description: How to manage publishable API keys
|
||||
url: https://docs.medusajs.com/development/publishable-api-keys/admin/manage-publishable-api-keys
|
||||
- name: Regions
|
||||
description: >
|
||||
Regions are different countries or geographical regions that the commerce
|
||||
store serves customers in.
|
||||
|
||||
Admins can manage these regions, their providers, and more.
|
||||
externalDocs:
|
||||
description: How to manage regions
|
||||
url: https://docs.medusajs.com/modules/regions-and-currencies/admin/manage-regions
|
||||
- name: Reservations
|
||||
description: >
|
||||
Reservations, provided by the [Inventory
|
||||
Module](https://docs.medusajs.com/modules/multiwarehouse/inventory-module),
|
||||
are quantities of an item that are reserved, typically when an order is
|
||||
placed but not yet fulfilled.
|
||||
|
||||
Reservations can be associated with any resources, but commonly with line items of an order.
|
||||
externalDocs:
|
||||
description: How to manage item allocations in orders
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-item-allocations-in-orders
|
||||
- name: Return Reasons
|
||||
description: >
|
||||
Return reasons are key-value pairs that are used to specify why an order
|
||||
return is being created.
|
||||
|
||||
Admins can manage available return reasons, and they can be used by both admins and customers when creating a return.
|
||||
externalDocs:
|
||||
description: How to manage return reasons
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-returns#manage-return-reasons
|
||||
- name: Returns
|
||||
description: >
|
||||
A return can be created by a customer or an admin to return items in an
|
||||
order.
|
||||
|
||||
Admins can manage these returns and change their state.
|
||||
externalDocs:
|
||||
description: How to manage returns
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-returns
|
||||
- name: Sales Channels
|
||||
description: >
|
||||
A sales channel indicates a channel where products can be sold in. For
|
||||
example, a webshop or a mobile app.
|
||||
|
||||
Admins can manage sales channels and the products available in them.
|
||||
externalDocs:
|
||||
description: How to manage sales channels
|
||||
url: https://docs.medusajs.com/modules/sales-channels/admin/manage
|
||||
- name: Shipping Options
|
||||
description: >
|
||||
A shipping option is used to define the available shipping methods during
|
||||
checkout or when creating a return.
|
||||
|
||||
Admins can create an unlimited number of shipping options, each associated with a shipping profile and fulfillment provider, among other resources.
|
||||
externalDocs:
|
||||
description: Shipping Option architecture
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-option
|
||||
- name: Shipping Profiles
|
||||
description: >
|
||||
A shipping profile is used to group products that can be shipped in the
|
||||
same manner.
|
||||
|
||||
They are created by the admin and they're not associated with a fulfillment provider.
|
||||
externalDocs:
|
||||
description: Shipping Profile architecture
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-profile
|
||||
- name: Stock Locations
|
||||
description: >
|
||||
A stock location, provided by the [Stock Location
|
||||
module](https://docs.medusajs.com/modules/multiwarehouse/stock-location-module),
|
||||
indicates a physical address that stock-kept items, such as physical
|
||||
products, can be stored in.
|
||||
|
||||
An admin can create and manage available stock locations.
|
||||
externalDocs:
|
||||
description: How to manage stock locations.
|
||||
url: https://docs.medusajs.com/modules/multiwarehouse/admin/manage-stock-locations
|
||||
- name: Store
|
||||
description: >
|
||||
A store indicates the general configurations and details about the
|
||||
commerce store. By default, there's only one store in the Medusa backend.
|
||||
|
||||
Admins can manage the store and its details or configurations.
|
||||
- name: Swaps
|
||||
description: >
|
||||
A swap is created by a customer or an admin to exchange an item with a new
|
||||
one.
|
||||
|
||||
Creating a swap implicitely includes creating a return for the item being exchanged.
|
||||
externalDocs:
|
||||
description: How to manage swaps
|
||||
url: https://docs.medusajs.com/modules/orders/admin/manage-swaps
|
||||
- name: Tax Rates
|
||||
description: >
|
||||
Each region has at least a default tax rate. Admins can create and manage
|
||||
additional tax rates that can be applied for certain conditions, such as
|
||||
for specific product types.
|
||||
externalDocs:
|
||||
description: How to manage tax rates
|
||||
url: https://docs.medusajs.com/modules/taxes/admin/manage-tax-rates
|
||||
- name: Uploads
|
||||
description: >
|
||||
The upload API Routes are used to upload any type of resources. For
|
||||
example, they can be used to upload CSV files that are used to import
|
||||
products into the store.
|
||||
externalDocs:
|
||||
description: How to upload CSV file when importing a product.
|
||||
url: https://docs.medusajs.com/modules/products/admin/import-products#1-upload-csv-file
|
||||
- name: Users
|
||||
description: >
|
||||
A store can have more than one user, each having the same privileges.
|
||||
Admins can manage users, their passwords, and more.
|
||||
externalDocs:
|
||||
description: How to manage users
|
||||
url: https://docs.medusajs.com/modules/users/admin/manage-users
|
||||
servers:
|
||||
- url: http://localhost:9000
|
||||
- url: https://api.medusa-commerce.com
|
||||
paths: {}
|
||||
components:
|
||||
responses:
|
||||
default_error:
|
||||
description: Default Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: unknown_error
|
||||
message: An unknown error occurred.
|
||||
type: unknown_error
|
||||
invalid_state_error:
|
||||
description: Invalid State Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: unknown_error
|
||||
message: The request conflicted with another request. You may retry the request
|
||||
with the provided Idempotency-Key.
|
||||
type: QueryRunnerAlreadyReleasedError
|
||||
invalid_request_error:
|
||||
description: Invalid Request Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: invalid_request_error
|
||||
message: Discount with code TEST already exists.
|
||||
type: duplicate_error
|
||||
not_found_error:
|
||||
description: Not Found Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
message: Entity with id 1 was not found
|
||||
type: not_found
|
||||
400_error:
|
||||
description: Client Error or Multiple Errors
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Error"
|
||||
- $ref: "#/components/schemas/MultipleErrors"
|
||||
examples:
|
||||
not_allowed:
|
||||
$ref: "#/components/examples/not_allowed_error"
|
||||
invalid_data:
|
||||
$ref: "#/components/examples/invalid_data_error"
|
||||
MultipleErrors:
|
||||
$ref: "#/components/examples/multiple_errors"
|
||||
500_error:
|
||||
description: Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
examples:
|
||||
database:
|
||||
$ref: "#/components/examples/database_error"
|
||||
unexpected_state:
|
||||
$ref: "#/components/examples/unexpected_state_error"
|
||||
invalid_argument:
|
||||
$ref: "#/components/examples/invalid_argument_error"
|
||||
default_error:
|
||||
$ref: "#/components/examples/default_error"
|
||||
unauthorized:
|
||||
description: User is not authorized. Must log in first
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
default: Unauthorized
|
||||
example: Unauthorized
|
||||
incorrect_credentials:
|
||||
description: User does not exist or incorrect credentials
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
default: Unauthorized
|
||||
example: Unauthorized
|
||||
examples:
|
||||
not_allowed_error:
|
||||
summary: Not Allowed Error
|
||||
value:
|
||||
message: Discount must be set to dynamic
|
||||
type: not_allowed
|
||||
invalid_data_error:
|
||||
summary: Invalid Data Error
|
||||
value:
|
||||
message: first_name must be a string
|
||||
type: invalid_data
|
||||
multiple_errors:
|
||||
summary: Multiple Errors
|
||||
value:
|
||||
message: Provided request body contains errors. Please check the data and retry
|
||||
the request
|
||||
errors:
|
||||
- message: first_name must be a string
|
||||
type: invalid_data
|
||||
- message: Discount must be set to dynamic
|
||||
type: not_allowed
|
||||
database_error:
|
||||
summary: Database Error
|
||||
value:
|
||||
code: api_error
|
||||
message: An error occured while hashing password
|
||||
type: database_error
|
||||
unexpected_state_error:
|
||||
summary: Unexpected State Error
|
||||
value:
|
||||
message: cart.total must be defined
|
||||
type: unexpected_state
|
||||
invalid_argument_error:
|
||||
summary: Invalid Argument Error
|
||||
value:
|
||||
message: cart.total must be defined
|
||||
type: unexpected_state
|
||||
default_error:
|
||||
summary: Default Error
|
||||
value:
|
||||
code: unknown_error
|
||||
message: An unknown error occurred.
|
||||
type: unknown_error
|
||||
securitySchemes:
|
||||
api_token:
|
||||
type: apiKey
|
||||
x-displayName: API Token
|
||||
in: header
|
||||
name: x-medusa-access-token
|
||||
jwt_token:
|
||||
type: http
|
||||
x-displayName: JWT Token
|
||||
scheme: bearer
|
||||
cookie_auth:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: connect.sid
|
||||
x-displayName: Cookie Session ID
|
||||
287
docs-util/oas-output/base/store.oas.base.yaml
Normal file
287
docs-util/oas-output/base/store.oas.base.yaml
Normal file
@@ -0,0 +1,287 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Medusa Storefront API
|
||||
license:
|
||||
name: MIT
|
||||
url: https://github.com/medusajs/medusa/blob/master/LICENSE
|
||||
tags:
|
||||
- name: Auth
|
||||
description: |
|
||||
Authentication API Routes allow you to manage a customer's session, such as login or log out.
|
||||
You can send authenticated requests for a customer either using the Cookie header or using the JWT Token.
|
||||
externalDocs:
|
||||
description: How to implement customer profiles in your storefront
|
||||
url: https://docs.medusajs.com/modules/customers/storefront/implement-customer-profiles
|
||||
- name: Carts
|
||||
description: |
|
||||
A cart is a virtual shopping bag that customers can use to add items they want to purchase.
|
||||
A cart is then used to checkout and place an order.
|
||||
externalDocs:
|
||||
description: How to implement cart functionality in your storefront
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/storefront/implement-cart
|
||||
- name: Customers
|
||||
description: |
|
||||
A customer can register and manage their information such as addresses, orders, payment methods, and more.
|
||||
externalDocs:
|
||||
description: How to implement customer profiles in your storefront
|
||||
url: https://docs.medusajs.com/modules/customers/storefront/implement-customer-profiles
|
||||
- name: Gift Cards
|
||||
description: |
|
||||
Customers can use gift cards during checkout to deduct the gift card's balance from the checkout total.
|
||||
The Gift Card API Routes allow retrieving a gift card's details by its code. A gift card can be applied to a cart using the Carts API Routes.
|
||||
externalDocs:
|
||||
description: How to use gift cards in a storefront
|
||||
url: https://docs.medusajs.com/modules/gift-cards/storefront/use-gift-cards
|
||||
- name: Orders
|
||||
description: |
|
||||
Orders are purchases made by customers, typically through a storefront.
|
||||
Orders are placed and created using the Carts API Routes. The Orders API Routes allow retrieving and claiming orders.
|
||||
externalDocs:
|
||||
description: How to retrieve order details in a storefront
|
||||
url: https://docs.medusajs.com/modules/orders/storefront/retrieve-order-details
|
||||
- name: Order Edits
|
||||
description: |
|
||||
Order edits are changes made to items in an order such as adding, updating their quantity, or deleting them. Order edits are created by the admin.
|
||||
A customer can review order edit requests created by an admin and confirm or decline them.
|
||||
externalDocs:
|
||||
description: How to handle order edits in a storefront
|
||||
url: https://docs.medusajs.com/modules/orders/storefront/handle-order-edits
|
||||
- name: Payment Collections
|
||||
description: |
|
||||
A payment collection is useful for managing additional payments, such as for Order Edits, or installment payments.
|
||||
- name: Products
|
||||
description: |
|
||||
Products are saleable items in a store. This also includes [saleable gift cards](https://docs.medusajs.com/modules/gift-cards/storefront/use-gift-cards) in a store.
|
||||
Using these API Routes, you can filter products by categories, collections, sales channels, and more.
|
||||
externalDocs:
|
||||
description: How to show products in a storefront
|
||||
url: https://docs.medusajs.com/modules/products/storefront/show-products
|
||||
- name: Product Variants
|
||||
description: |
|
||||
Product variants are the actual salable item in your store. Each variant is a combination of the different option values available on the product.
|
||||
- name: Product Categories
|
||||
description: |
|
||||
Products can be categoriezed into categories. A product can be associated more than one category.
|
||||
Using these API Routes, you can list or retrieve a category's details and products.
|
||||
externalDocs:
|
||||
description: How to use product categories in a storefront
|
||||
url: https://docs.medusajs.com/modules/products/storefront/use-categories
|
||||
- name: Product Collections
|
||||
description: |
|
||||
A product collection is used to organize products for different purposes such as marketing or discount purposes. For example, you can create a Summer Collection.
|
||||
Using these API Routes, you can list or retrieve a collection's details and products.
|
||||
- name: Product Tags
|
||||
description: |
|
||||
Product tags are string values that can be used to filter products by.
|
||||
Products can have more than one tag, and products can share tags.
|
||||
- name: Product Types
|
||||
description: |
|
||||
Product types are string values that can be used to filter products by.
|
||||
Products can have more than one tag, and products can share types.
|
||||
- name: Regions
|
||||
description: |
|
||||
Regions are different countries or geographical regions that the commerce store serves customers in.
|
||||
Customers can choose what region they're in, which can be used to change the prices shown based on the region and its currency.
|
||||
externalDocs:
|
||||
description: How to use regions in a storefront
|
||||
url: https://docs.medusajs.com/modules/regions-and-currencies/storefront/use-regions
|
||||
- name: Returns
|
||||
description: |
|
||||
A return can be created by a customer to return items in an order.
|
||||
externalDocs:
|
||||
description: How to create a return in a storefront
|
||||
url: https://docs.medusajs.com/modules/orders/storefront/create-return
|
||||
- name: Return Reasons
|
||||
description: |
|
||||
Return reasons are key-value pairs that are used to specify why an order return is being created.
|
||||
- name: Shipping Options
|
||||
description: |
|
||||
A shipping option is used to define the available shipping methods during checkout or when creating a return.
|
||||
externalDocs:
|
||||
description: Shipping Option architecture
|
||||
url: https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-option
|
||||
- name: Swaps
|
||||
description: |
|
||||
A swap is created by a customer or an admin to exchange an item with a new one.
|
||||
Creating a swap implicitely includes creating a return for the item being exchanged.
|
||||
externalDocs:
|
||||
description: How to create a swap in a storefront
|
||||
url: https://docs.medusajs.com/modules/orders/storefront/create-swap
|
||||
servers:
|
||||
- url: http://localhost:9000
|
||||
- url: https://api.medusa-commerce.com
|
||||
paths: { }
|
||||
components:
|
||||
responses:
|
||||
default_error:
|
||||
description: Default Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "unknown_error"
|
||||
message: "An unknown error occurred."
|
||||
type: "unknown_error"
|
||||
invalid_state_error:
|
||||
description: Invalid State Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "unknown_error"
|
||||
message: "The request conflicted with another request. You may retry the request with the provided Idempotency-Key."
|
||||
type: "QueryRunnerAlreadyReleasedError"
|
||||
invalid_request_error:
|
||||
description: Invalid Request Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
code: "invalid_request_error"
|
||||
message: "Discount with code TEST already exists."
|
||||
type: "duplicate_error"
|
||||
not_found_error:
|
||||
description: Not Found Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
example:
|
||||
message: "Entity with id 1 was not found"
|
||||
type: "not_found"
|
||||
400_error:
|
||||
description: Client Error or Multiple Errors
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/Error"
|
||||
- $ref: "#/components/schemas/MultipleErrors"
|
||||
examples:
|
||||
not_allowed:
|
||||
$ref: "#/components/examples/not_allowed_error"
|
||||
invalid_data:
|
||||
$ref: "#/components/examples/invalid_data_error"
|
||||
MultipleErrors:
|
||||
$ref: "#/components/examples/multiple_errors"
|
||||
500_error:
|
||||
description: Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
examples:
|
||||
database:
|
||||
$ref: "#/components/examples/database_error"
|
||||
unexpected_state:
|
||||
$ref: "#/components/examples/unexpected_state_error"
|
||||
invalid_argument:
|
||||
$ref: "#/components/examples/invalid_argument_error"
|
||||
default_error:
|
||||
$ref: "#/components/examples/default_error"
|
||||
unauthorized:
|
||||
description: 'User is not authorized. Must log in first'
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
default: Unauthorized
|
||||
example: Unauthorized
|
||||
incorrect_credentials:
|
||||
description: 'User does not exist or incorrect credentials'
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
default: Unauthorized
|
||||
example: Unauthorized
|
||||
examples:
|
||||
not_allowed_error:
|
||||
summary: Not Allowed Error
|
||||
value:
|
||||
message: "Discount must be set to dynamic"
|
||||
type: "not_allowed"
|
||||
invalid_data_error:
|
||||
summary: Invalid Data Error
|
||||
value:
|
||||
message: "first_name must be a string"
|
||||
type: "invalid_data"
|
||||
multiple_errors:
|
||||
summary: Multiple Errors
|
||||
value:
|
||||
message: "Provided request body contains errors. Please check the data and retry the request"
|
||||
errors:
|
||||
- message: "first_name must be a string"
|
||||
type: "invalid_data"
|
||||
- message: "Discount must be set to dynamic"
|
||||
type: "not_allowed"
|
||||
database_error:
|
||||
summary: Database Error
|
||||
value:
|
||||
code: "api_error"
|
||||
message: "An error occured while hashing password"
|
||||
type: "database_error"
|
||||
unexpected_state_error:
|
||||
summary: Unexpected State Error
|
||||
value:
|
||||
message: "cart.total must be defined"
|
||||
type: "unexpected_state"
|
||||
invalid_argument_error:
|
||||
summary: Invalid Argument Error
|
||||
value:
|
||||
message: "cart.total must be defined"
|
||||
type: "unexpected_state"
|
||||
default_error:
|
||||
summary: Default Error
|
||||
value:
|
||||
code: "unknown_error"
|
||||
message: "An unknown error occurred."
|
||||
type: "unknown_error"
|
||||
securitySchemes:
|
||||
jwt_token:
|
||||
type: http
|
||||
x-displayName: JWT Token
|
||||
scheme: bearer
|
||||
cookie_auth:
|
||||
type: apiKey
|
||||
x-displayName: Cookie Session ID
|
||||
in: cookie
|
||||
name: connect.sid
|
||||
description: |
|
||||
Use a cookie session to send authenticated requests.
|
||||
|
||||
### How to Obtain the Cookie Session
|
||||
|
||||
If you're sending requests through a browser, using JS Client, or using tools like Postman, the cookie session should be automatically set when the customer is logged in.
|
||||
|
||||
If you're sending requests using cURL, you must set the Session ID in the cookie manually.
|
||||
|
||||
To do that, send a request to [authenticate the customer](#tag/Auth/operation/PostAuth) and pass the cURL option `-v`:
|
||||
|
||||
```bash
|
||||
curl -v --location --request POST 'https://medusa-url.com/store/auth' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "user@example.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
The headers will be logged in the terminal as well as the response. You should find in the headers a Cookie header similar to this:
|
||||
|
||||
```bash
|
||||
Set-Cookie: connect.sid=s%3A2Bu8BkaP9JUfHu9rG59G16Ma0QZf6Gj1.WT549XqX37PN8n0OecqnMCq798eLjZC5IT7yiDCBHPM;
|
||||
```
|
||||
|
||||
Copy the value after `connect.sid` (without the `;` at the end) and pass it as a cookie in subsequent requests as the following:
|
||||
|
||||
```bash
|
||||
curl --location --request GET 'https://medusa-url.com/store/customers/me/orders' \
|
||||
--header 'Cookie: connect.sid={sid}'
|
||||
```
|
||||
|
||||
Where `{sid}` is the value of `connect.sid` that you copied.
|
||||
@@ -1,11 +1,12 @@
|
||||
# docblock-generator
|
||||
|
||||
A CLI tool that can be used to generate TSDoc docblocks for TypeScript/JavaScript files under the `packages` directory of the main monorepo.
|
||||
A CLI tool that can be used to generate TSDoc docblocks and OAS for TypeScript/JavaScript files under the `packages` directory of the main monorepo.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Run the `yarn` command to install dependencies.
|
||||
2. Copy the `.env.sample` to `.env` and change the `MONOREPO_ROOT_PATH` variable to the absolute path to the monorepo root.
|
||||
3. Run the `yarn build` command to build source files.
|
||||
|
||||
---
|
||||
|
||||
@@ -36,3 +37,55 @@ yarn start run:commit <commit-sha>
|
||||
```
|
||||
|
||||
Where `<commit-sha>` is the SHA of the commit. For example, `e28fa7fbdf45c5b1fa19848db731132a0bf1757d`.
|
||||
|
||||
### Generate for a release
|
||||
|
||||
Run the following command to run the tool on commits since the latest release.
|
||||
|
||||
```bash
|
||||
yarn start run:release
|
||||
```
|
||||
|
||||
### Clean OAS
|
||||
|
||||
Run the following command to clean up the OAS output files and remove any routes that no longer exist:
|
||||
|
||||
```bash
|
||||
yarn start clean:oas
|
||||
```
|
||||
|
||||
This command will also remove tags and schemas not used.
|
||||
|
||||
---
|
||||
|
||||
## How it Works
|
||||
|
||||
### Generating OAS
|
||||
|
||||
If a node is an API route, it generates OAS comments rather than TSDoc comments. The OAS comments are generated and placed in new/existing files under the `docs-util/oas-output/operations` directory.
|
||||
|
||||
### Generating TSDoc Docblocks
|
||||
|
||||
If a note isn't an API Route and it complies with the specified conditions, TSDoc docblocks are generated for it.
|
||||
|
||||
Files under the `packages/medusa/src/api` or `api-v2` directories are considered incompatible, so any files under these directories won't have TSDoc docblocks generated for them.
|
||||
|
||||
---
|
||||
|
||||
## Common Options
|
||||
|
||||
This section includes options that you can pass to any of the mentioned commands.
|
||||
|
||||
### --generate-examples
|
||||
|
||||
If this option is passed, the tool will try to generate OAS examples. Currently, it will only try to generate JS Client examples.
|
||||
|
||||
cURL examples are always generated regardless of this option.
|
||||
|
||||
### --type
|
||||
|
||||
You can use this option to specify the type of docs to generate. Possible values are:
|
||||
|
||||
- `all`: (default) Generate all doc types.
|
||||
- `docs`: Generate only TSDoc docblocks.
|
||||
- `oas`: Generate only OAS docblocks/files.
|
||||
|
||||
@@ -18,14 +18,19 @@
|
||||
"workflow-diagrams-generator": "dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^8.4.0",
|
||||
"@octokit/core": "^5.0.2",
|
||||
"commander": "^11.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"openapi-types": "^12.1.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.2",
|
||||
"utils": "*"
|
||||
"utils": "*",
|
||||
"yaml": "^2.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.9.4"
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { faker } from "@faker-js/faker"
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
import { API_ROUTE_PARAM_REGEX, OasArea } from "../kinds/oas.js"
|
||||
import {
|
||||
capitalize,
|
||||
kebabToCamel,
|
||||
wordsToCamel,
|
||||
wordsToKebab,
|
||||
} from "../../utils/str-formatting.js"
|
||||
import { CodeSample } from "../../types/index.js"
|
||||
|
||||
type CodeSampleData = Omit<CodeSample, "source">
|
||||
|
||||
/**
|
||||
* This class generates examples for OAS.
|
||||
*/
|
||||
class OasExamplesGenerator {
|
||||
static JSCLIENT_CODESAMPLE_DATA: CodeSampleData = {
|
||||
lang: "JavaScript",
|
||||
label: "JS Client",
|
||||
}
|
||||
static CURL_CODESAMPLE_DATA: CodeSampleData = {
|
||||
lang: "Shell",
|
||||
label: "cURL",
|
||||
}
|
||||
static MEDUSAREACT_CODESAMPLE_DATA: CodeSampleData = {
|
||||
lang: "tsx",
|
||||
label: "Medusa React",
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JS client example for an OAS operation.
|
||||
*
|
||||
* @param param0 - The operation's details
|
||||
* @returns The JS client example.
|
||||
*/
|
||||
generateJSClientExample({
|
||||
area,
|
||||
tag,
|
||||
oasPath,
|
||||
httpMethod,
|
||||
isAdminAuthenticated,
|
||||
isStoreAuthenticated,
|
||||
parameters,
|
||||
requestBody,
|
||||
responseBody,
|
||||
}: {
|
||||
/**
|
||||
* The area of the operation.
|
||||
*/
|
||||
area: OasArea
|
||||
/**
|
||||
* The tag this operation belongs to.
|
||||
*/
|
||||
tag: string
|
||||
/**
|
||||
* The API route's path.
|
||||
*/
|
||||
oasPath: string
|
||||
/**
|
||||
* The http method of the operation.
|
||||
*/
|
||||
httpMethod: string
|
||||
/**
|
||||
* Whether the operation requires admin authentication.
|
||||
*/
|
||||
isAdminAuthenticated?: boolean
|
||||
/**
|
||||
* Whether the operation requires customer authentication.
|
||||
*/
|
||||
isStoreAuthenticated?: boolean
|
||||
/**
|
||||
* The path parameters that can be sent in the request, if any.
|
||||
*/
|
||||
parameters?: OpenAPIV3.ParameterObject[]
|
||||
/**
|
||||
* The request body's schema, if any.
|
||||
*/
|
||||
requestBody?: OpenAPIV3.SchemaObject
|
||||
/**
|
||||
* The response body's schema, if any.
|
||||
*/
|
||||
responseBody?: OpenAPIV3.SchemaObject
|
||||
}) {
|
||||
const exampleArr = [
|
||||
`import Medusa from "@medusajs/medusa-js"`,
|
||||
`const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })`,
|
||||
]
|
||||
|
||||
if (isAdminAuthenticated) {
|
||||
exampleArr.push(`// must be previously logged in or use api token`)
|
||||
} else if (isStoreAuthenticated) {
|
||||
exampleArr.push(`// must be previously logged in.`)
|
||||
}
|
||||
|
||||
// infer JS method name
|
||||
// reset regex manually
|
||||
API_ROUTE_PARAM_REGEX.lastIndex = 0
|
||||
const isForSingleEntity = API_ROUTE_PARAM_REGEX.test(oasPath)
|
||||
let jsMethod = `{methodName}`
|
||||
if (isForSingleEntity) {
|
||||
const splitOasPath = oasPath
|
||||
.replaceAll(API_ROUTE_PARAM_REGEX, "")
|
||||
.replace(/\/(batch)*$/, "")
|
||||
.split("/")
|
||||
const isBulk = oasPath.endsWith("/batch")
|
||||
const isOperationOnDifferentEntity =
|
||||
wordsToKebab(tag) !== splitOasPath[splitOasPath.length - 1]
|
||||
if (isBulk || isOperationOnDifferentEntity) {
|
||||
const endingEntityName = capitalize(
|
||||
isBulk &&
|
||||
API_ROUTE_PARAM_REGEX.test(splitOasPath[splitOasPath.length - 1])
|
||||
? wordsToCamel(tag)
|
||||
: kebabToCamel(splitOasPath[splitOasPath.length - 1])
|
||||
)
|
||||
|
||||
jsMethod =
|
||||
httpMethod === "get"
|
||||
? `list${endingEntityName}`
|
||||
: httpMethod === "post"
|
||||
? `add${endingEntityName}`
|
||||
: `remove${endingEntityName}`
|
||||
} else {
|
||||
jsMethod =
|
||||
httpMethod === "get"
|
||||
? "retrieve"
|
||||
: httpMethod === "post"
|
||||
? "update"
|
||||
: "delete"
|
||||
}
|
||||
} else {
|
||||
jsMethod =
|
||||
httpMethod === "get"
|
||||
? "list"
|
||||
: httpMethod === "post"
|
||||
? "create"
|
||||
: "delete"
|
||||
}
|
||||
|
||||
// collect the path/request parameters to be passed to the request.
|
||||
const parametersArr: string[] =
|
||||
parameters?.map((parameter) => parameter.name) || []
|
||||
const requestData = requestBody
|
||||
? this.getSchemaRequiredData(requestBody)
|
||||
: {}
|
||||
|
||||
// assemble the method-call line of format `medusa.{admin?}.{methodName}({...parameters,} {requestBodyDataObj})`
|
||||
exampleArr.push(
|
||||
`medusa${area === "admin" ? `.${area}` : ""}.${wordsToCamel(
|
||||
tag
|
||||
)}.${jsMethod}(${parametersArr.join(", ")}${
|
||||
Object.keys(requestData).length
|
||||
? `${parametersArr.length ? ", " : ""}${JSON.stringify(
|
||||
requestData,
|
||||
undefined,
|
||||
2
|
||||
)}`
|
||||
: ""
|
||||
})`
|
||||
)
|
||||
|
||||
// assemble then lines with response data, if any
|
||||
const responseData = responseBody
|
||||
? this.getSchemaRequiredData(responseBody)
|
||||
: {}
|
||||
const responseRequiredItems = Object.keys(responseData)
|
||||
const responseRequiredItemsStr = responseRequiredItems.length
|
||||
? `{ ${responseRequiredItems.join(", ")} }`
|
||||
: ""
|
||||
|
||||
exampleArr.push(
|
||||
`.then((${responseRequiredItemsStr}) => {\n\t\t${
|
||||
responseRequiredItemsStr.length
|
||||
? `console.log(${responseRequiredItemsStr})`
|
||||
: "// Success"
|
||||
}\n})`
|
||||
)
|
||||
|
||||
return exampleArr.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cURL examples for an OAS operation.
|
||||
*
|
||||
* @param param0 - The operation's details.
|
||||
* @returns The cURL example.
|
||||
*/
|
||||
generateCurlExample({
|
||||
method,
|
||||
path,
|
||||
isAdminAuthenticated,
|
||||
isStoreAuthenticated,
|
||||
requestSchema,
|
||||
}: {
|
||||
/**
|
||||
* The HTTP method.
|
||||
*/
|
||||
method: string
|
||||
/**
|
||||
* The API Route's path.
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* Whether the route requires admin authentication.
|
||||
*/
|
||||
isAdminAuthenticated?: boolean
|
||||
/**
|
||||
* Whether the route requires customer authentication.
|
||||
*/
|
||||
isStoreAuthenticated?: boolean
|
||||
/**
|
||||
* The schema of the request body, if any.
|
||||
*/
|
||||
requestSchema?: OpenAPIV3.SchemaObject
|
||||
}): string {
|
||||
const exampleArr = [
|
||||
`curl${
|
||||
method.toLowerCase() !== "get" ? ` -X ${method.toUpperCase()}` : ""
|
||||
} '{backend_url}${path}'`,
|
||||
]
|
||||
|
||||
if (isAdminAuthenticated) {
|
||||
exampleArr.push(`-H 'x-medusa-access-token: {api_token}'`)
|
||||
} else if (isStoreAuthenticated) {
|
||||
exampleArr.push(`-H 'Authorization: Bearer {access_token}'`)
|
||||
}
|
||||
|
||||
if (requestSchema) {
|
||||
const requestData = this.getSchemaRequiredData(requestSchema)
|
||||
|
||||
if (Object.keys(requestData).length > 0) {
|
||||
exampleArr.push(`-H 'Content-Type: application/json'`)
|
||||
exampleArr.push(
|
||||
`--data-raw '${JSON.stringify(requestData, undefined, 2)}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return exampleArr.join(` \\\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves data object from a schema object. Only retrieves the required fields.
|
||||
*
|
||||
* @param schema - The schema to retrieve its required data object.
|
||||
* @returns An object of required data and their fake values.
|
||||
*/
|
||||
getSchemaRequiredData(
|
||||
schema: OpenAPIV3.SchemaObject
|
||||
): Record<string, unknown> {
|
||||
const data: Record<string, unknown> = {}
|
||||
|
||||
if (schema.required?.length && schema.properties) {
|
||||
schema.required.forEach((propertyName) => {
|
||||
// extract property and its type
|
||||
const property = schema.properties![
|
||||
propertyName
|
||||
] as OpenAPIV3.SchemaObject
|
||||
let value: unknown
|
||||
if (property.type === "object") {
|
||||
const typedValue: Record<string, unknown> = {}
|
||||
// get the fake value of every property in the object
|
||||
if (property.properties) {
|
||||
Object.entries(property.properties).forEach(
|
||||
([childName, childProp]) => {
|
||||
const typedChildProp = childProp as OpenAPIV3.SchemaObject
|
||||
if (!typedChildProp.type) {
|
||||
return
|
||||
}
|
||||
// if the property is an object, get its data object
|
||||
// otherwise, get its fake value
|
||||
typedValue[childName] =
|
||||
typedChildProp.type === "object"
|
||||
? this.getSchemaRequiredData(
|
||||
typedChildProp as OpenAPIV3.SchemaObject
|
||||
)
|
||||
: this.getFakeValue({
|
||||
name: childName,
|
||||
type: typedChildProp.type,
|
||||
format: typedChildProp.format,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
value = typedValue
|
||||
} else if (property.type === "array") {
|
||||
// if the type of the array's items is an object, retrieve
|
||||
// its data object. Otherwise, retrieve its fake value.
|
||||
const propertyItems = property.items as OpenAPIV3.SchemaObject
|
||||
if (!propertyItems.type) {
|
||||
value = []
|
||||
} else {
|
||||
value = [
|
||||
propertyItems.type === "object"
|
||||
? this.getSchemaRequiredData(
|
||||
property.items as OpenAPIV3.SchemaObject
|
||||
)
|
||||
: this.getFakeValue({
|
||||
name: propertyName,
|
||||
type: propertyItems.type,
|
||||
format: propertyItems.format,
|
||||
}),
|
||||
]
|
||||
}
|
||||
} else if (property.type) {
|
||||
// retrieve fake value for all other types
|
||||
value = this.getFakeValue({
|
||||
name: propertyName,
|
||||
type: property.type,
|
||||
format: property.format,
|
||||
})
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
data[propertyName] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the fake value of a property. The value is used in examples.
|
||||
*
|
||||
* @param param0 - The property's details
|
||||
* @returns The fake value
|
||||
*/
|
||||
getFakeValue({
|
||||
name,
|
||||
type,
|
||||
format,
|
||||
}: {
|
||||
/**
|
||||
* The name of the property. It can help when generating the fake value.
|
||||
* For example, if the name is `id`, the fake value generated will be of the format `id_<randomstring>`.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* The type of the property.
|
||||
*/
|
||||
type: OpenAPIV3.NonArraySchemaObjectType | "array"
|
||||
/**
|
||||
* The OAS format of the property. For example, `date-time`.
|
||||
*/
|
||||
format?: string
|
||||
}): unknown {
|
||||
let value: unknown
|
||||
|
||||
switch (true) {
|
||||
case type === "string" && format === "date-time":
|
||||
value = faker.date.future().toISOString()
|
||||
break
|
||||
case type === "boolean":
|
||||
value = faker.datatype.boolean()
|
||||
break
|
||||
case type === "integer" || type === "number":
|
||||
value = faker.number.int()
|
||||
break
|
||||
case type === "array":
|
||||
value = []
|
||||
break
|
||||
case type === "string":
|
||||
value = faker.helpers
|
||||
.mustache(`{{${name}}}`, {
|
||||
id: () =>
|
||||
`id_${faker.string.alphanumeric({
|
||||
length: { min: 10, max: 20 },
|
||||
})}`,
|
||||
name: () => faker.person.firstName(),
|
||||
email: () => faker.internet.email(),
|
||||
password: () => faker.internet.password({ length: 8 }),
|
||||
currency: () => faker.finance.currencyCode(),
|
||||
})
|
||||
.replace(`{{${name}}}`, "{value}")
|
||||
}
|
||||
|
||||
return value !== undefined ? value : "{value}"
|
||||
}
|
||||
}
|
||||
|
||||
export default OasExamplesGenerator
|
||||
@@ -1,54 +1,35 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import ts from "typescript"
|
||||
import Formatter from "./formatter.js"
|
||||
import KindsRegistry from "./kinds/registry.js"
|
||||
import nodeHasComments from "../utils/node-has-comments.js"
|
||||
|
||||
export type Options = {
|
||||
paths: string[]
|
||||
dryRun?: boolean
|
||||
}
|
||||
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
import AbstractGenerator from "./index.js"
|
||||
import { minimatch } from "minimatch"
|
||||
|
||||
/**
|
||||
* A class used to generate docblock for one or multiple file paths.
|
||||
*/
|
||||
class DocblockGenerator {
|
||||
protected options: Options
|
||||
protected program?: ts.Program
|
||||
protected checker?: ts.TypeChecker
|
||||
protected formatter: Formatter
|
||||
protected kindsRegistry?: KindsRegistry
|
||||
|
||||
constructor(options: Options) {
|
||||
this.options = options
|
||||
this.formatter = new Formatter()
|
||||
}
|
||||
|
||||
class DocblockGenerator extends AbstractGenerator {
|
||||
/**
|
||||
* Generate the docblock for the paths specified in the {@link options} class property.
|
||||
* Generate docblocks for the files in the `options`.
|
||||
*/
|
||||
async run() {
|
||||
this.program = ts.createProgram(this.options.paths, {})
|
||||
|
||||
this.checker = this.program.getTypeChecker()
|
||||
|
||||
this.kindsRegistry = new KindsRegistry(this.checker)
|
||||
this.init()
|
||||
|
||||
const printer = ts.createPrinter({
|
||||
removeComments: false,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
this.program.getSourceFiles().map(async (file) => {
|
||||
this.program!.getSourceFiles().map(async (file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Generating for ${file.fileName}...`)
|
||||
console.log(`[Docblock] Generating for ${file.fileName}...`)
|
||||
|
||||
let fileContent = file.getFullText()
|
||||
let fileComments: string = ""
|
||||
const commentsToRemove: string[] = []
|
||||
|
||||
const documentChild = (node: ts.Node, topLevel = false) => {
|
||||
const isSourceFile = ts.isSourceFile(node)
|
||||
@@ -56,7 +37,7 @@ class DocblockGenerator {
|
||||
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
|
||||
let docComment: string | undefined
|
||||
|
||||
if (nodeKindGenerator && this.canDocumentNode(node)) {
|
||||
if (nodeKindGenerator?.canDocumentNode(node)) {
|
||||
docComment = nodeKindGenerator.getDocBlock(node)
|
||||
if (docComment.length) {
|
||||
if (isSourceFile) {
|
||||
@@ -92,6 +73,9 @@ class DocblockGenerator {
|
||||
documentChild(file, true)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
commentsToRemove.forEach((commentToRemove) => {
|
||||
fileContent = fileContent.replace(commentToRemove, "")
|
||||
})
|
||||
ts.sys.writeFile(
|
||||
file.fileName,
|
||||
this.formatter.addCommentsToSourceFile(
|
||||
@@ -101,44 +85,30 @@ class DocblockGenerator {
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Finished generating docblock for ${file.fileName}.`)
|
||||
console.log(
|
||||
`[Docblock] Finished generating docblock for ${file.fileName}.`
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
this.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a file is included in the specified files.
|
||||
* Checks whether the specified file path is included in the program
|
||||
* and isn't an API file.
|
||||
*
|
||||
* @param {string} fileName - The file to check for.
|
||||
* @returns {boolean} Whether the file can have docblocks generated for it.
|
||||
* @param fileName - The file path to check
|
||||
* @returns Whether the docblock generator can run on this file.
|
||||
*/
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
return this.options.paths.some((path) => path.includes(fileName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node can be documented.
|
||||
*
|
||||
* @privateRemark
|
||||
* I'm leaving this method in case other conditions arise for a node to be documented.
|
||||
* Otherwise, we can directly use the {@link nodeHasComments} function.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check for.
|
||||
* @returns {boolean} Whether the node can be documented.
|
||||
*/
|
||||
canDocumentNode(node: ts.Node): boolean {
|
||||
// check if node already has docblock
|
||||
return !nodeHasComments(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the generator's properties for new usage.
|
||||
*/
|
||||
reset() {
|
||||
this.program = undefined
|
||||
this.checker = undefined
|
||||
return (
|
||||
super.isFileIncluded(fileName) &&
|
||||
!minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
matchBase: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import ts from "typescript"
|
||||
import Formatter from "../helpers/formatter.js"
|
||||
import KindsRegistry from "../kinds/registry.js"
|
||||
import GeneratorEventManager from "../helpers/generator-event-manager.js"
|
||||
import { CommonCliOptions } from "../../types/index.js"
|
||||
import { existsSync, readdirSync, statSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
export type Options = {
|
||||
paths: string[]
|
||||
dryRun?: boolean
|
||||
} & Pick<CommonCliOptions, "generateExamples">
|
||||
|
||||
abstract class AbstractGenerator {
|
||||
protected options: Options
|
||||
protected program?: ts.Program
|
||||
protected checker?: ts.TypeChecker
|
||||
protected formatter: Formatter
|
||||
protected kindsRegistry?: KindsRegistry
|
||||
protected generatorEventManager: GeneratorEventManager
|
||||
|
||||
constructor(options: Options) {
|
||||
this.options = options
|
||||
this.formatter = new Formatter()
|
||||
this.generatorEventManager = new GeneratorEventManager()
|
||||
}
|
||||
|
||||
init() {
|
||||
const files: string[] = []
|
||||
|
||||
this.options.paths.forEach((optionPath) => {
|
||||
if (!existsSync(optionPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!statSync(optionPath).isDirectory()) {
|
||||
files.push(optionPath)
|
||||
return
|
||||
}
|
||||
|
||||
// read files recursively from directory
|
||||
files.push(
|
||||
...readdirSync(optionPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.map((filePath) => path.join(optionPath, filePath))
|
||||
.filter((filePath) => !statSync(filePath).isDirectory())
|
||||
)
|
||||
})
|
||||
|
||||
this.program = ts.createProgram(files, {})
|
||||
|
||||
this.checker = this.program.getTypeChecker()
|
||||
|
||||
const { generateExamples } = this.options
|
||||
|
||||
this.kindsRegistry = new KindsRegistry({
|
||||
checker: this.checker,
|
||||
generatorEventManager: this.generatorEventManager,
|
||||
additionalOptions: {
|
||||
generateExamples,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the docblock for the paths specified in the {@link options} class property.
|
||||
*/
|
||||
abstract run(): void
|
||||
|
||||
/**
|
||||
* Checks whether a file is included in the specified files.
|
||||
*
|
||||
* @param {string} fileName - The file to check for.
|
||||
* @returns {boolean} Whether the file can have docblocks generated for it.
|
||||
*/
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
const baseFilePath = this.getBasePath(fileName)
|
||||
return this.options.paths.some((path) =>
|
||||
baseFilePath.startsWith(this.getBasePath(path))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the pathname of a file without the relative part before `packages/`
|
||||
*
|
||||
* @param fileName - The file name/path
|
||||
* @returns The path without the relative part.
|
||||
*/
|
||||
getBasePath(fileName: string) {
|
||||
let basePath = fileName
|
||||
const packageIndex = fileName.indexOf("packages/")
|
||||
if (packageIndex) {
|
||||
basePath = basePath.substring(packageIndex)
|
||||
}
|
||||
|
||||
return basePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the generator's properties for new usage.
|
||||
*/
|
||||
reset() {
|
||||
this.program = undefined
|
||||
this.checker = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default AbstractGenerator
|
||||
@@ -0,0 +1,76 @@
|
||||
import { minimatch } from "minimatch"
|
||||
import AbstractGenerator from "./index.js"
|
||||
import ts from "typescript"
|
||||
import OasKindGenerator from "../kinds/oas.js"
|
||||
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
|
||||
/**
|
||||
* A class used to generate OAS yaml comments. The comments are written
|
||||
* in different files than the specified files.
|
||||
*/
|
||||
class OasGenerator extends AbstractGenerator {
|
||||
protected oasKindGenerator?: OasKindGenerator
|
||||
|
||||
run() {
|
||||
this.init()
|
||||
|
||||
const { generateExamples } = this.options
|
||||
|
||||
this.oasKindGenerator = new OasKindGenerator({
|
||||
checker: this.checker!,
|
||||
generatorEventManager: this.generatorEventManager,
|
||||
additionalOptions: {
|
||||
generateExamples,
|
||||
},
|
||||
})
|
||||
|
||||
this.program!.getSourceFiles().map((file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[OAS] Generating for ${file.fileName}...`)
|
||||
|
||||
const documentChild = (node: ts.Node) => {
|
||||
if (
|
||||
this.oasKindGenerator!.isAllowed(node) &&
|
||||
this.oasKindGenerator!.canDocumentNode(node)
|
||||
) {
|
||||
const oas = this.oasKindGenerator!.getDocBlock(node)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
const filename = this.oasKindGenerator!.getAssociatedFileName(node)
|
||||
ts.sys.writeFile(
|
||||
filename,
|
||||
this.formatter.addCommentsToSourceFile(oas, "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(file, documentChild)
|
||||
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
console.log(`[OAS] Finished generating OAS for ${file.fileName}.`)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified file path is included in the program
|
||||
* and is an API file.
|
||||
*
|
||||
* @param fileName - The file path to check
|
||||
* @returns Whether the OAS generator can run on this file.
|
||||
*/
|
||||
isFileIncluded(fileName: string): boolean {
|
||||
return (
|
||||
super.isFileIncluded(fileName) &&
|
||||
minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", {
|
||||
matchBase: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default OasGenerator
|
||||
@@ -1,10 +1,11 @@
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import getMonorepoRoot from "../../utils/get-monorepo-root.js"
|
||||
import { ESLint, Linter } from "eslint"
|
||||
import path from "path"
|
||||
import dirname from "../utils/dirname.js"
|
||||
import dirname from "../../utils/dirname.js"
|
||||
import { minimatch } from "minimatch"
|
||||
import { existsSync } from "fs"
|
||||
import getRelativePaths from "../utils/get-relative-paths.js"
|
||||
import * as prettier from "prettier"
|
||||
import getRelativePaths from "../../utils/get-relative-paths.js"
|
||||
|
||||
/**
|
||||
* A class used to apply formatting to files using ESLint and other formatting options.
|
||||
@@ -70,10 +71,10 @@ class Formatter {
|
||||
)
|
||||
|
||||
newConfig.parserOptions.project = [
|
||||
existsSync(tsConfigSpecPath)
|
||||
? tsConfigSpecPath
|
||||
: existsSync(tsConfigPath)
|
||||
? tsConfigPath
|
||||
existsSync(tsConfigPath)
|
||||
? tsConfigPath
|
||||
: existsSync(tsConfigSpecPath)
|
||||
? tsConfigSpecPath
|
||||
: [
|
||||
...getRelativePaths(
|
||||
newConfig.parserOptions.project || [],
|
||||
@@ -170,6 +171,10 @@ class Formatter {
|
||||
content: string,
|
||||
fileName: string
|
||||
): Promise<string> {
|
||||
const prettifiedContent = await this.formatStrWithPrettier(
|
||||
content,
|
||||
fileName
|
||||
)
|
||||
const relevantConfig = await this.getESLintOverridesConfigForFile(fileName)
|
||||
|
||||
const eslint = new ESLint({
|
||||
@@ -183,8 +188,8 @@ class Formatter {
|
||||
ignore: false,
|
||||
})
|
||||
|
||||
let newContent = content
|
||||
const result = await eslint.lintText(content, {
|
||||
let newContent = prettifiedContent
|
||||
const result = await eslint.lintText(prettifiedContent, {
|
||||
filePath: fileName,
|
||||
})
|
||||
|
||||
@@ -195,6 +200,27 @@ class Formatter {
|
||||
return newContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a file's content with prettier.
|
||||
*
|
||||
* @param content - The content to format.
|
||||
* @param fileName - The name of the file the content belongs to.
|
||||
* @returns The formatted content
|
||||
*/
|
||||
async formatStrWithPrettier(
|
||||
content: string,
|
||||
fileName: string
|
||||
): Promise<string> {
|
||||
// load config of the file
|
||||
const prettierConfig = (await prettier.resolveConfig(fileName)) || undefined
|
||||
|
||||
if (prettierConfig && !prettierConfig.parser) {
|
||||
prettierConfig.parser = "babel-ts"
|
||||
}
|
||||
|
||||
return await prettier.format(content, prettierConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all formatting types to a string.
|
||||
*
|
||||
@@ -0,0 +1,37 @@
|
||||
import EventEmitter from "events"
|
||||
|
||||
export enum GeneratorEvent {
|
||||
FINISHED_GENERATE_EVENT = "finished_generate",
|
||||
}
|
||||
|
||||
/**
|
||||
* A class used to emit events during the lifecycle of the generator.
|
||||
*/
|
||||
class GeneratorEventManager {
|
||||
private eventEmitter: EventEmitter
|
||||
|
||||
constructor() {
|
||||
this.eventEmitter = new EventEmitter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to listeners.
|
||||
*
|
||||
* @param event - The event to emit.
|
||||
*/
|
||||
emit(event: GeneratorEvent) {
|
||||
this.eventEmitter.emit(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to an event.
|
||||
*
|
||||
* @param event - The event to add a listener for.
|
||||
* @param handler - The handler of the event.
|
||||
*/
|
||||
listen(event: GeneratorEvent, handler: () => void) {
|
||||
this.eventEmitter.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
export default GeneratorEventManager
|
||||
@@ -1,10 +1,12 @@
|
||||
import ts from "typescript"
|
||||
import { DOCBLOCK_DOUBLE_LINES, DOCBLOCK_NEW_LINE } from "../constants.js"
|
||||
import { DOCBLOCK_DOUBLE_LINES, DOCBLOCK_NEW_LINE } from "../../constants.js"
|
||||
import {
|
||||
camelToTitle,
|
||||
camelToWords,
|
||||
normalizeName,
|
||||
} from "../utils/str-formatting.js"
|
||||
snakeToWords,
|
||||
} from "../../utils/str-formatting.js"
|
||||
import pluralize from "pluralize"
|
||||
|
||||
type TemplateOptions = {
|
||||
parentName?: string
|
||||
@@ -16,7 +18,10 @@ type KnowledgeBase = {
|
||||
startsWith?: string
|
||||
endsWith?: string
|
||||
exact?: string
|
||||
template: string | ((str: string, options?: TemplateOptions) => string)
|
||||
pattern?: RegExp
|
||||
template:
|
||||
| string
|
||||
| ((str: string, options?: TemplateOptions) => string | undefined)
|
||||
kind?: ts.SyntaxKind[]
|
||||
}
|
||||
|
||||
@@ -213,6 +218,27 @@ class KnowledgeBaseFactory {
|
||||
template: `An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the {type name} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.`,
|
||||
},
|
||||
]
|
||||
private oasDescriptionKnowledgeBase: KnowledgeBase[] = [
|
||||
{
|
||||
pattern: /.*/,
|
||||
template(str, options) {
|
||||
if (!options?.parentName) {
|
||||
return
|
||||
}
|
||||
|
||||
const formattedName = str === "id" ? "ID" : snakeToWords(str)
|
||||
const formattedParentName = pluralize.singular(
|
||||
snakeToWords(options.parentName)
|
||||
)
|
||||
|
||||
if (formattedName === formattedParentName) {
|
||||
return `The ${formattedParentName}'s details.`
|
||||
}
|
||||
|
||||
return `The ${formattedParentName}'s ${formattedName}.`
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Tries to find in a specified knowledge base a template relevant to the specified name.
|
||||
@@ -235,6 +261,10 @@ class KnowledgeBaseFactory {
|
||||
return str === item.exact
|
||||
}
|
||||
|
||||
if (item.pattern) {
|
||||
return item.pattern.test(str)
|
||||
}
|
||||
|
||||
if (item.kind?.length && (!kind || !item.kind.includes(kind))) {
|
||||
return false
|
||||
}
|
||||
@@ -320,6 +350,23 @@ class KnowledgeBaseFactory {
|
||||
knowledgeBase: this.functionReturnKnowledgeBase,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve the description template of an OAS property from the {@link oasDescriptionKnowledgeBase}.
|
||||
*
|
||||
* @returns {string | undefined} The matching knowledgebase template, if found.
|
||||
*/
|
||||
tryToGetOasDescription({
|
||||
str,
|
||||
...options
|
||||
}: RetrieveOptions): string | undefined {
|
||||
const normalizedTypeStr = str.replaceAll("[]", "")
|
||||
return this.tryToFindInKnowledgeBase({
|
||||
...options,
|
||||
str: normalizedTypeStr,
|
||||
knowledgeBase: this.oasDescriptionKnowledgeBase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default KnowledgeBaseFactory
|
||||
@@ -0,0 +1,233 @@
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
import { OpenApiSchema } from "../../types/index.js"
|
||||
import Formatter from "./formatter.js"
|
||||
import { join } from "path"
|
||||
import { DOCBLOCK_LINE_ASTRIX } from "../../constants.js"
|
||||
import ts from "typescript"
|
||||
import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js"
|
||||
import { parse } from "yaml"
|
||||
import formatOas from "../../utils/format-oas.js"
|
||||
import pluralize from "pluralize"
|
||||
import { wordsToPascal } from "../../utils/str-formatting.js"
|
||||
|
||||
type ParsedSchema = {
|
||||
schema: OpenApiSchema
|
||||
schemaPrefix: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Class providing helper methods for OAS Schemas
|
||||
*/
|
||||
class OasSchemaHelper {
|
||||
/**
|
||||
* This map collects schemas created while generating the OAS, then, once the generation process
|
||||
* finishes, it checks if it should be added to the base OAS document.
|
||||
*/
|
||||
private schemas: Map<string, OpenApiSchema>
|
||||
protected schemaRefPrefix = "#/components/schemas/"
|
||||
protected formatter: Formatter
|
||||
/**
|
||||
* The path to the directory holding the base YAML files.
|
||||
*/
|
||||
protected baseOutputPath: string
|
||||
|
||||
constructor() {
|
||||
this.schemas = new Map()
|
||||
this.formatter = new Formatter()
|
||||
this.baseOutputPath = getOasOutputBasePath()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the {@link schemas} property. Helpful when resetting the property.
|
||||
*/
|
||||
init() {
|
||||
this.schemas = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve schema as a reference object and add the schema to the {@link schemas} property.
|
||||
*
|
||||
* @param schema - The schema to convert and add to the schemas property.
|
||||
* @returns The schema as a reference. If the schema doesn't have the x-schemaName property set,
|
||||
* the schema isn't converted and `undefined` is returned.
|
||||
*/
|
||||
schemaToReference(
|
||||
schema: OpenApiSchema
|
||||
): OpenAPIV3.ReferenceObject | undefined {
|
||||
if (!schema["x-schemaName"]) {
|
||||
return
|
||||
}
|
||||
schema["x-schemaName"] = this.normalizeSchemaName(schema["x-schemaName"])
|
||||
|
||||
// check if schema has child schemas
|
||||
// and convert those
|
||||
if (schema.properties) {
|
||||
Object.keys(schema.properties).forEach((property) => {
|
||||
if (
|
||||
"$ref" in schema.properties![property] ||
|
||||
!(schema.properties![property] as OpenApiSchema)["x-schemaName"]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
schema.properties![property] =
|
||||
this.schemaToReference(
|
||||
schema.properties![property] as OpenApiSchema
|
||||
) || schema.properties![property]
|
||||
})
|
||||
}
|
||||
|
||||
this.schemas.set(schema["x-schemaName"], schema)
|
||||
|
||||
return {
|
||||
$ref: this.constructSchemaReference(schema["x-schemaName"]),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expected file name of the schema.
|
||||
*
|
||||
* @param name - The schema's name
|
||||
* @returns The schema's file name
|
||||
*/
|
||||
getSchemaFileName(name: string): string {
|
||||
return join(
|
||||
this.baseOutputPath,
|
||||
"schemas",
|
||||
`${this.normalizeSchemaName(name)}.ts`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the schema by its name. If the schema is in the {@link schemas} map, it'll be retrieved from
|
||||
* there. Otherwise, the method will try to retrieve it from an outputted schema file, if available.
|
||||
*
|
||||
* @param name - The schema's name.
|
||||
* @returns The parsed schema, if found.
|
||||
*/
|
||||
getSchemaByName(name: string): ParsedSchema | undefined {
|
||||
const schemaName = this.normalizeSchemaName(name)
|
||||
// check if it already exists in the schemas map
|
||||
if (this.schemas.has(schemaName)) {
|
||||
return {
|
||||
schema: this.schemas.get(schemaName)!,
|
||||
schemaPrefix: `@schema ${schemaName}`,
|
||||
}
|
||||
}
|
||||
const schemaFile = this.getSchemaFileName(schemaName)
|
||||
const schemaFileContent = ts.sys.readFile(schemaFile)
|
||||
|
||||
if (!schemaFileContent) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.parseSchema(schemaFileContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a schema comment string.
|
||||
*
|
||||
* @param content - The schema comment string
|
||||
* @returns If the schema is valid and parsed successfully, the schema and its prefix are retrieved.
|
||||
*/
|
||||
parseSchema(content: string): ParsedSchema | undefined {
|
||||
const schemaFileContent = content
|
||||
.replace(`/**\n`, "")
|
||||
.replaceAll(DOCBLOCK_LINE_ASTRIX, "")
|
||||
.replaceAll("*/", "")
|
||||
.trim()
|
||||
|
||||
if (!schemaFileContent.startsWith("@schema")) {
|
||||
return
|
||||
}
|
||||
|
||||
const splitContent = schemaFileContent.split("\n")
|
||||
const schemaPrefix = splitContent[0]
|
||||
let schema: OpenApiSchema | undefined
|
||||
|
||||
try {
|
||||
schema = parse(splitContent.slice(1).join("\n"))
|
||||
} catch (e) {
|
||||
// couldn't parse the OAS, so consider it
|
||||
// not existent
|
||||
}
|
||||
|
||||
return schema
|
||||
? {
|
||||
schema,
|
||||
schemaPrefix,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the normalized schema name. A schema's name must be normalized before saved.
|
||||
*
|
||||
* @param name - The original name.
|
||||
* @returns The normalized name.
|
||||
*/
|
||||
normalizeSchemaName(name: string): string {
|
||||
return name.replace("DTO", "").replace(this.schemaRefPrefix, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a reference string to a schema.
|
||||
*
|
||||
* @param name - The name of the schema. For cautionary reasons, the name is normalized using the {@link normalizeSchemaName} method.
|
||||
* @returns The schema reference.
|
||||
*/
|
||||
constructSchemaReference(name: string): string {
|
||||
return `${this.schemaRefPrefix}${this.normalizeSchemaName(name)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes schemas in the {@link schemas} property to the file path retrieved using the {@link getSchemaFileName} method.
|
||||
*/
|
||||
writeNewSchemas() {
|
||||
this.schemas.forEach((schema) => {
|
||||
if (!schema["x-schemaName"]) {
|
||||
return
|
||||
}
|
||||
const normalizedName = this.normalizeSchemaName(schema["x-schemaName"])
|
||||
const schemaFileName = this.getSchemaFileName(normalizedName)
|
||||
|
||||
ts.sys.writeFile(
|
||||
schemaFileName,
|
||||
this.formatter.addCommentsToSourceFile(
|
||||
formatOas(schema, `@schema ${normalizedName}`),
|
||||
""
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an object is a reference object.
|
||||
*
|
||||
* @param schema - The schema object to check.
|
||||
* @returns Whether the object is a reference object.
|
||||
*/
|
||||
isRefObject(
|
||||
schema:
|
||||
| OpenAPIV3.ReferenceObject
|
||||
| OpenApiSchema
|
||||
| OpenAPIV3.RequestBodyObject
|
||||
| OpenAPIV3.ResponseObject
|
||||
| undefined
|
||||
): schema is OpenAPIV3.ReferenceObject {
|
||||
return schema !== undefined && "$ref" in schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tag name to a schema name. Can be used to try and retrieve the schema
|
||||
* associated with a tag.
|
||||
*
|
||||
* @param tagName - The name of the tag.
|
||||
* @returns The possible name of the associated schema.
|
||||
*/
|
||||
tagNameToSchemaName(tagName: string): string {
|
||||
return wordsToPascal(pluralize.singular(tagName))
|
||||
}
|
||||
}
|
||||
|
||||
export default OasSchemaHelper
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import getSymbol from "../../utils/get-symbol.js"
|
||||
import KnowledgeBaseFactory, {
|
||||
RetrieveOptions,
|
||||
} from "../knowledge-base-factory.js"
|
||||
} from "../helpers/knowledge-base-factory.js"
|
||||
import {
|
||||
getCustomNamespaceTag,
|
||||
shouldHaveCustomNamespace,
|
||||
@@ -18,10 +18,14 @@ import {
|
||||
capitalize,
|
||||
normalizeName,
|
||||
} from "../../utils/str-formatting.js"
|
||||
import GeneratorEventManager from "../helpers/generator-event-manager.js"
|
||||
import { CommonCliOptions } from "../../types/index.js"
|
||||
|
||||
export type GeneratorOptions = {
|
||||
checker: ts.TypeChecker
|
||||
kinds?: ts.SyntaxKind[]
|
||||
generatorEventManager: GeneratorEventManager
|
||||
additionalOptions: Pick<CommonCliOptions, "generateExamples">
|
||||
}
|
||||
|
||||
export type GetDocBlockOptions = {
|
||||
@@ -54,11 +58,20 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
protected checker: ts.TypeChecker
|
||||
protected defaultSummary = "{summary}"
|
||||
protected knowledgeBaseFactory: KnowledgeBaseFactory
|
||||
protected generatorEventManager: GeneratorEventManager
|
||||
protected options: Pick<CommonCliOptions, "generateExamples">
|
||||
|
||||
constructor({ checker, kinds }: GeneratorOptions) {
|
||||
constructor({
|
||||
checker,
|
||||
kinds,
|
||||
generatorEventManager,
|
||||
additionalOptions,
|
||||
}: GeneratorOptions) {
|
||||
this.allowedKinds = kinds || DefaultKindGenerator.DEFAULT_ALLOWED_NODE_KINDS
|
||||
this.checker = checker
|
||||
this.knowledgeBaseFactory = new KnowledgeBaseFactory()
|
||||
this.generatorEventManager = generatorEventManager
|
||||
this.options = additionalOptions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +149,11 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
*/
|
||||
nodeType?: ts.Type
|
||||
}): string {
|
||||
const knowledgeBaseOptions = this.getKnowledgeOptions(node)
|
||||
const syntheticComments = ts.getSyntheticLeadingComments(node)
|
||||
if (syntheticComments?.length) {
|
||||
return syntheticComments.map((comment) => comment.text).join(" ")
|
||||
}
|
||||
const knowledgeBaseOptions = this.getKnowledgeBaseOptions(node)
|
||||
if (!nodeType) {
|
||||
nodeType =
|
||||
"type" in node && node.type && ts.isTypeNode(node.type as ts.Node)
|
||||
@@ -170,7 +187,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
* @param {ts.Type} nodeType - The type of a node.
|
||||
* @returns {string} The summary comment.
|
||||
*/
|
||||
private getTypeDocBlock(
|
||||
protected getTypeDocBlock(
|
||||
nodeType: ts.Type,
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
): string {
|
||||
@@ -226,7 +243,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
* @param {ts.Symbol} symbol - The symbol to retrieve its docblock.
|
||||
* @returns {string} The symbol's docblock.
|
||||
*/
|
||||
private getSymbolDocBlock(
|
||||
protected getSymbolDocBlock(
|
||||
symbol: ts.Symbol,
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
): string {
|
||||
@@ -395,26 +412,9 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
}
|
||||
|
||||
// check for default value
|
||||
if (
|
||||
"initializer" in node &&
|
||||
node.initializer &&
|
||||
ts.isExpression(node.initializer as ts.Node)
|
||||
) {
|
||||
const initializer = node.initializer as ts.Expression
|
||||
|
||||
// retrieve default value only if the value is numeric, string, or boolean
|
||||
const defaultValue =
|
||||
ts.isNumericLiteral(initializer) || ts.isStringLiteral(initializer)
|
||||
? initializer.getText()
|
||||
: initializer.kind === ts.SyntaxKind.FalseKeyword
|
||||
? "false"
|
||||
: initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
? "true"
|
||||
: ""
|
||||
|
||||
if (defaultValue.length) {
|
||||
tags.add(`@defaultValue ${defaultValue}`)
|
||||
}
|
||||
const defaultValue = this.getDefaultValue(node)
|
||||
if (defaultValue?.length) {
|
||||
tags.add(`@defaultValue ${defaultValue}`)
|
||||
}
|
||||
|
||||
let str = ""
|
||||
@@ -481,7 +481,13 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
)
|
||||
}
|
||||
|
||||
getKnowledgeOptions(node: ts.Node): Partial<RetrieveOptions> {
|
||||
/**
|
||||
* Get knowledge base options for a specified node.
|
||||
*
|
||||
* @param node - The node to retrieve its knowledge base options.
|
||||
* @returns The knowledge base options.
|
||||
*/
|
||||
getKnowledgeBaseOptions(node: ts.Node): Partial<RetrieveOptions> {
|
||||
const rawParentName =
|
||||
"name" in node.parent &&
|
||||
node.parent.name &&
|
||||
@@ -498,6 +504,88 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value of a node.
|
||||
*
|
||||
* @param node - The node to get its default value.
|
||||
* @returns The default value, if any.
|
||||
*/
|
||||
getDefaultValue(node: ts.Node): string | undefined {
|
||||
if (
|
||||
"initializer" in node &&
|
||||
node.initializer &&
|
||||
ts.isExpression(node.initializer as ts.Node)
|
||||
) {
|
||||
const initializer = node.initializer as ts.Expression
|
||||
|
||||
// retrieve default value only if the value is numeric, string, or boolean
|
||||
const defaultValue =
|
||||
ts.isNumericLiteral(initializer) || ts.isStringLiteral(initializer)
|
||||
? initializer.getText()
|
||||
: initializer.kind === ts.SyntaxKind.FalseKeyword
|
||||
? "false"
|
||||
: initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
? "true"
|
||||
: ""
|
||||
|
||||
if (defaultValue.length) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node can be documented.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check for.
|
||||
* @returns {boolean} Whether the node can be documented.
|
||||
*/
|
||||
canDocumentNode(node: ts.Node): boolean {
|
||||
// check if node already has docblock
|
||||
return !this.nodeHasComments(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments range of a node.
|
||||
* @param node - The node to get its comment range.
|
||||
* @returns The comment range of the node if available.
|
||||
*/
|
||||
getNodeCommentsRange(node: ts.Node): ts.CommentRange[] | undefined {
|
||||
return ts.getLeadingCommentRanges(
|
||||
node.getSourceFile().getFullText(),
|
||||
node.getFullStart()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node's comment from its range.
|
||||
*
|
||||
* @param node - The node to get its comment range.
|
||||
* @returns The comment if available.
|
||||
*/
|
||||
getNodeCommentsFromRange(node: ts.Node): string | undefined {
|
||||
const commentRange = this.getNodeCommentsRange(node)
|
||||
|
||||
if (!commentRange?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return node
|
||||
.getSourceFile()
|
||||
.getFullText()
|
||||
.slice(commentRange[0].pos, commentRange[0].end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a node has comments.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
* @returns Whether the node has comments.
|
||||
*/
|
||||
nodeHasComments(node: ts.Node): boolean {
|
||||
return this.getNodeCommentsFromRange(node) !== undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default DefaultKindGenerator
|
||||
|
||||
@@ -14,7 +14,7 @@ export type FunctionNode =
|
||||
| ts.FunctionDeclaration
|
||||
| ts.ArrowFunction
|
||||
|
||||
type VariableNode = ts.VariableDeclaration | ts.VariableStatement
|
||||
export type VariableNode = ts.VariableDeclaration | ts.VariableStatement
|
||||
|
||||
export type FunctionOrVariableNode = FunctionNode | ts.VariableStatement
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
DOCBLOCK_START,
|
||||
DOCBLOCK_DOUBLE_LINES,
|
||||
} from "../../constants.js"
|
||||
import nodeHasComments from "../../utils/node-has-comments.js"
|
||||
import {
|
||||
CUSTOM_NAMESPACE_TAG,
|
||||
getCustomNamespaceTag,
|
||||
@@ -152,7 +151,7 @@ class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
|
||||
return (
|
||||
!parameterTypeStr?.startsWith("UseQueryOptionsWrapper") &&
|
||||
!parameterTypeStr?.startsWith("UseMutationOptions") &&
|
||||
!nodeHasComments(parameter)
|
||||
!this.nodeHasComments(parameter)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
1870
docs-util/packages/docblock-generator/src/classes/kinds/oas.ts
Normal file
1870
docs-util/packages/docblock-generator/src/classes/kinds/oas.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
import ts from "typescript"
|
||||
import FunctionKindGenerator from "./function.js"
|
||||
import DefaultKindGenerator from "./default.js"
|
||||
import DefaultKindGenerator, { GeneratorOptions } from "./default.js"
|
||||
import MedusaReactHooksKindGenerator from "./medusa-react-hooks.js"
|
||||
import SourceFileKindGenerator from "./source-file.js"
|
||||
import DTOPropertyGenerator from "./dto-property.js"
|
||||
import OasKindGenerator from "./oas.js"
|
||||
|
||||
/**
|
||||
* A class that is used as a registry for the kind generators.
|
||||
@@ -12,14 +13,20 @@ class KindsRegistry {
|
||||
protected kindInstances: DefaultKindGenerator[]
|
||||
protected defaultKindGenerator: DefaultKindGenerator
|
||||
|
||||
constructor(checker: ts.TypeChecker) {
|
||||
constructor(
|
||||
options: Pick<
|
||||
GeneratorOptions,
|
||||
"checker" | "generatorEventManager" | "additionalOptions"
|
||||
>
|
||||
) {
|
||||
this.kindInstances = [
|
||||
new MedusaReactHooksKindGenerator({ checker }),
|
||||
new FunctionKindGenerator({ checker }),
|
||||
new SourceFileKindGenerator({ checker }),
|
||||
new DTOPropertyGenerator({ checker }),
|
||||
new OasKindGenerator(options),
|
||||
new MedusaReactHooksKindGenerator(options),
|
||||
new FunctionKindGenerator(options),
|
||||
new SourceFileKindGenerator(options),
|
||||
new DTOPropertyGenerator(options),
|
||||
]
|
||||
this.defaultKindGenerator = new DefaultKindGenerator({ checker })
|
||||
this.defaultKindGenerator = new DefaultKindGenerator(options)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
271
docs-util/packages/docblock-generator/src/commands/clean-oas.ts
Normal file
271
docs-util/packages/docblock-generator/src/commands/clean-oas.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import path from "path"
|
||||
import getOasOutputBasePath from "../utils/get-oas-output-base-path.js"
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "fs"
|
||||
import parseOas from "../utils/parse-oas.js"
|
||||
import OasKindGenerator, { OasArea } from "../classes/kinds/oas.js"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import ts from "typescript"
|
||||
import GeneratorEventManager from "../classes/helpers/generator-event-manager.js"
|
||||
import { parse, stringify } from "yaml"
|
||||
import OasSchemaHelper from "../classes/helpers/oas-schema.js"
|
||||
import { DEFAULT_OAS_RESPONSES } from "../constants.js"
|
||||
import { OpenApiDocument } from "../types/index.js"
|
||||
|
||||
const OAS_PREFIX_REGEX = /@oas \[(?<method>(get|post|delete))\] (?<path>.+)/
|
||||
|
||||
export default async function () {
|
||||
const oasOutputBasePath = getOasOutputBasePath()
|
||||
const oasOperationsPath = path.join(oasOutputBasePath, "operations")
|
||||
const apiRoutesPath = path.join(
|
||||
getMonorepoRoot(),
|
||||
"packages",
|
||||
"medusa",
|
||||
"src",
|
||||
"api-v2"
|
||||
)
|
||||
const areas: OasArea[] = ["admin", "store"]
|
||||
const tags: Map<OasArea, Set<string>> = new Map()
|
||||
const oasSchemaHelper = new OasSchemaHelper()
|
||||
const referencedSchemas: Set<string> = new Set()
|
||||
const allSchemas: Set<string> = new Set()
|
||||
areas.forEach((area) => {
|
||||
tags.set(area, new Set<string>())
|
||||
})
|
||||
|
||||
console.log("Cleaning OAS files...")
|
||||
|
||||
// read files under the operations/{area} directory
|
||||
areas.forEach((area) => {
|
||||
const areaPath = path.join(oasOperationsPath, area)
|
||||
if (!existsSync(areaPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
readdirSync(areaPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((oasFile) => {
|
||||
const filePath = path.join(areaPath, oasFile)
|
||||
const { oas, oasPrefix } = parseOas(readFileSync(filePath, "utf-8")) || {}
|
||||
|
||||
if (!oas || !oasPrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
// decode oasPrefix
|
||||
const matchOasPrefix = OAS_PREFIX_REGEX.exec(oasPrefix)
|
||||
if (!matchOasPrefix?.groups?.method || !matchOasPrefix.groups.path) {
|
||||
return
|
||||
}
|
||||
const splitPath = matchOasPrefix.groups.path.substring(1).split("/")
|
||||
|
||||
// normalize path by replacing {paramName} with [paramName]
|
||||
const normalizedOasPrefix = splitPath
|
||||
.map((item) => item.replace(/^\{(.+)\}$/, "[$1]"))
|
||||
.join("/")
|
||||
const sourceFilePath = path.join(
|
||||
apiRoutesPath,
|
||||
normalizedOasPrefix,
|
||||
"route.ts"
|
||||
)
|
||||
|
||||
// check if a route exists for the path
|
||||
if (!existsSync(sourceFilePath)) {
|
||||
// remove OAS file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// check if method exists in the file
|
||||
let exists = false
|
||||
const program = ts.createProgram([sourceFilePath], {})
|
||||
|
||||
const oasKindGenerator = new OasKindGenerator({
|
||||
checker: program.getTypeChecker(),
|
||||
generatorEventManager: new GeneratorEventManager(),
|
||||
additionalOptions: {},
|
||||
})
|
||||
const sourceFile = program.getSourceFile(sourceFilePath)
|
||||
|
||||
if (!sourceFile) {
|
||||
// remove file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const visitChildren = (node: ts.Node) => {
|
||||
if (
|
||||
!exists &&
|
||||
oasKindGenerator.isAllowed(node) &&
|
||||
oasKindGenerator.canDocumentNode(node) &&
|
||||
oasKindGenerator.getHTTPMethodName(node) ===
|
||||
matchOasPrefix.groups!.method
|
||||
) {
|
||||
exists = true
|
||||
} else if (!exists) {
|
||||
ts.forEachChild(node, visitChildren)
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(sourceFile, visitChildren)
|
||||
|
||||
if (!exists) {
|
||||
// remove OAS file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// collect tags
|
||||
oas.tags?.forEach((tag) => {
|
||||
const areaTags = tags.get(area as OasArea)
|
||||
areaTags?.add(tag)
|
||||
})
|
||||
|
||||
// collect schemas
|
||||
if (oas.requestBody) {
|
||||
if (oasSchemaHelper.isRefObject(oas.requestBody)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(oas.requestBody.$ref)
|
||||
)
|
||||
} else {
|
||||
const requestBodySchema =
|
||||
oas.requestBody.content[Object.keys(oas.requestBody.content)[0]]
|
||||
.schema
|
||||
if (oasSchemaHelper.isRefObject(requestBodySchema)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(requestBodySchema.$ref)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oas.responses) {
|
||||
const successResponseKey = Object.keys(oas.responses)[0]
|
||||
if (!Object.keys(DEFAULT_OAS_RESPONSES).includes(successResponseKey)) {
|
||||
const responseObj = oas.responses[successResponseKey]
|
||||
if (oasSchemaHelper.isRefObject(responseObj)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(responseObj.$ref)
|
||||
)
|
||||
} else if (responseObj.content) {
|
||||
const responseBodySchema =
|
||||
responseObj.content[Object.keys(responseObj.content)[0]].schema
|
||||
if (oasSchemaHelper.isRefObject(responseBodySchema)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(responseBodySchema.$ref)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log("Clean tags...")
|
||||
|
||||
// check if any tags should be removed
|
||||
const oasBasePath = path.join(oasOutputBasePath, "base-v2")
|
||||
readdirSync(oasBasePath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((baseYaml) => {
|
||||
const baseYamlPath = path.join(oasBasePath, baseYaml)
|
||||
const parsedBaseYaml = parse(
|
||||
readFileSync(baseYamlPath, "utf-8")
|
||||
) as OpenApiDocument
|
||||
|
||||
const area = path.basename(baseYaml).split(".")[0] as OasArea
|
||||
const areaTags = tags.get(area)
|
||||
if (!areaTags) {
|
||||
return
|
||||
}
|
||||
const lengthBefore = parsedBaseYaml.tags?.length || 0
|
||||
|
||||
parsedBaseYaml.tags = parsedBaseYaml.tags?.filter((tag) =>
|
||||
areaTags.has(tag.name)
|
||||
)
|
||||
|
||||
if (lengthBefore !== (parsedBaseYaml.tags?.length || 0)) {
|
||||
// sort alphabetically
|
||||
parsedBaseYaml.tags?.sort((tagA, tagB) => {
|
||||
return tagA.name.localeCompare(tagB.name)
|
||||
})
|
||||
// write to the file
|
||||
writeFileSync(baseYamlPath, stringify(parsedBaseYaml))
|
||||
}
|
||||
|
||||
// collect referenced schemas
|
||||
parsedBaseYaml.tags?.forEach((tag) => {
|
||||
if (tag["x-associatedSchema"]) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(tag["x-associatedSchema"].$ref)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log("Clean schemas...")
|
||||
|
||||
// check if any schemas should be removed
|
||||
// a schema is removed if no other schemas/operations reference it
|
||||
const oasSchemasPath = path.join(oasOutputBasePath, "schemas")
|
||||
readdirSync(oasSchemasPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((schemaYaml) => {
|
||||
const schemaPath = path.join(oasSchemasPath, schemaYaml)
|
||||
const parsedSchema = oasSchemaHelper.parseSchema(
|
||||
readFileSync(schemaPath, "utf-8")
|
||||
)
|
||||
|
||||
if (!parsedSchema) {
|
||||
// remove file
|
||||
rmSync(schemaPath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// add schema to all schemas
|
||||
if (parsedSchema.schema["x-schemaName"]) {
|
||||
allSchemas.add(parsedSchema.schema["x-schemaName"])
|
||||
}
|
||||
|
||||
// collect referenced schemas
|
||||
if (parsedSchema.schema.properties) {
|
||||
Object.values(parsedSchema.schema.properties).forEach((property) => {
|
||||
if (oasSchemaHelper.isRefObject(property)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(property.$ref)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// clean up schemas
|
||||
allSchemas.forEach((schemaName) => {
|
||||
if (referencedSchemas.has(schemaName)) {
|
||||
return
|
||||
}
|
||||
|
||||
// schema isn't referenced anywhere, so remove it
|
||||
rmSync(path.join(oasSchemasPath, `${schemaName}.ts`), {
|
||||
force: true,
|
||||
})
|
||||
})
|
||||
|
||||
console.log("Finished clean up")
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import path from "path"
|
||||
import DocblockGenerator from "../classes/docblock-generator.js"
|
||||
import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import { GitManager } from "../classes/git-manager.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
|
||||
export default async function runGitChanges() {
|
||||
export default async function runGitChanges({
|
||||
type,
|
||||
...options
|
||||
}: CommonCliOptions) {
|
||||
const monorepoPath = getMonorepoRoot()
|
||||
// retrieve the changed files under `packages` in the monorepo root.
|
||||
const gitManager = new GitManager()
|
||||
@@ -20,12 +25,23 @@ export default async function runGitChanges() {
|
||||
|
||||
files = files.map((filePath) => path.resolve(monorepoPath, filePath))
|
||||
|
||||
// generate docblocks for each of the files.
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: files,
|
||||
})
|
||||
if (type === "all" || type === "docs") {
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: files,
|
||||
...options,
|
||||
})
|
||||
|
||||
await docblockGenerator.run()
|
||||
await docblockGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "oas") {
|
||||
const oasGenerator = new OasGenerator({
|
||||
paths: files,
|
||||
...options,
|
||||
})
|
||||
|
||||
oasGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${files.length} files.`)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import filterFiles from "../utils/filter-files.js"
|
||||
import path from "path"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import DocblockGenerator from "../classes/docblock-generator.js"
|
||||
import { GitManager } from "../classes/git-manager.js"
|
||||
import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
|
||||
export default async function (commitSha: string) {
|
||||
export default async function (
|
||||
commitSha: string,
|
||||
{ type, ...options }: CommonCliOptions
|
||||
) {
|
||||
const monorepoPath = getMonorepoRoot()
|
||||
// retrieve the files changed in the commit
|
||||
const gitManager = new GitManager()
|
||||
@@ -28,11 +33,23 @@ export default async function (commitSha: string) {
|
||||
)
|
||||
|
||||
// generate docblocks for each of the files.
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: filteredFiles,
|
||||
})
|
||||
if (type === "all" || type === "docs") {
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
await docblockGenerator.run()
|
||||
await docblockGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "oas") {
|
||||
const oasGenerator = new OasGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
oasGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import filterFiles from "../utils/filter-files.js"
|
||||
import path from "path"
|
||||
import DocblockGenerator from "../classes/docblock-generator.js"
|
||||
import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import getMonorepoRoot from "../utils/get-monorepo-root.js"
|
||||
import { GitManager } from "../classes/git-manager.js"
|
||||
import { GitManager } from "../classes/helpers/git-manager.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
|
||||
type Options = {
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export default async function ({ tag }: Options) {
|
||||
export default async function ({ type, tag, ...options }: CommonCliOptions) {
|
||||
const gitManager = new GitManager()
|
||||
|
||||
console.log(`Get files in commits since ${tag || "last release"}`)
|
||||
@@ -33,12 +31,23 @@ export default async function ({ tag }: Options) {
|
||||
path.resolve(getMonorepoRoot(), filePath)
|
||||
)
|
||||
|
||||
// generate docblocks for each of the files.
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: filteredFiles,
|
||||
})
|
||||
if (type === "all" || type === "docs") {
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
await docblockGenerator.run()
|
||||
await docblockGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "oas") {
|
||||
const oasGenerator = new OasGenerator({
|
||||
paths: filteredFiles,
|
||||
...options,
|
||||
})
|
||||
|
||||
oasGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished generating docs for ${filteredFiles.length} files.`)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import DocblockGenerator, { Options } from "../classes/docblock-generator.js"
|
||||
import DocblockGenerator from "../classes/generators/docblock.js"
|
||||
import { Options } from "../classes/generators/index.js"
|
||||
import OasGenerator from "../classes/generators/oas.js"
|
||||
import { CommonCliOptions } from "../types/index.js"
|
||||
|
||||
export default async function run(
|
||||
paths: string[],
|
||||
options: Omit<Options, "paths">
|
||||
{ type, ...options }: Omit<Options, "paths"> & CommonCliOptions
|
||||
) {
|
||||
console.log("Running...")
|
||||
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths,
|
||||
...options,
|
||||
})
|
||||
if (type === "all" || type === "docs") {
|
||||
const docblockGenerator = new DocblockGenerator({
|
||||
paths,
|
||||
...options,
|
||||
})
|
||||
|
||||
await docblockGenerator.run()
|
||||
await docblockGenerator.run()
|
||||
}
|
||||
|
||||
if (type === "all" || type === "oas") {
|
||||
const oasGenerator = new OasGenerator({
|
||||
paths,
|
||||
...options,
|
||||
})
|
||||
|
||||
oasGenerator.run()
|
||||
}
|
||||
|
||||
console.log(`Finished running.`)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,29 @@
|
||||
export const DOCBLOCK_NEW_LINE = "\n * "
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
|
||||
export const DOCBLOCK_LINE_ASTRIX = " * "
|
||||
export const DOCBLOCK_NEW_LINE = `\n${DOCBLOCK_LINE_ASTRIX}`
|
||||
export const DOCBLOCK_START = `*${DOCBLOCK_NEW_LINE}`
|
||||
export const DOCBLOCK_END_LINE = "\n"
|
||||
export const DOCBLOCK_DOUBLE_LINES = `${DOCBLOCK_NEW_LINE}${DOCBLOCK_NEW_LINE}`
|
||||
export const DEFAULT_OAS_RESPONSES: {
|
||||
[k: string]: OpenAPIV3.ReferenceObject
|
||||
} = {
|
||||
"400": {
|
||||
$ref: "#/components/responses/400_error",
|
||||
},
|
||||
"401": {
|
||||
$ref: "#/components/responses/unauthorized",
|
||||
},
|
||||
"404": {
|
||||
$ref: "#/components/responses/not_found_error",
|
||||
},
|
||||
"409": {
|
||||
$ref: "#/components/responses/invalid_state_error",
|
||||
},
|
||||
"422": {
|
||||
$ref: "#/components/responses/invalid_request_error",
|
||||
},
|
||||
"500": {
|
||||
$ref: "#/components/responses/500_error",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
import "dotenv/config"
|
||||
import { Command } from "commander"
|
||||
import { Command, Option } from "commander"
|
||||
import run from "./commands/run.js"
|
||||
import runGitChanges from "./commands/run-git-changes.js"
|
||||
import runGitCommit from "./commands/run-git-commit.js"
|
||||
import runRelease from "./commands/run-release.js"
|
||||
import cleanOas from "./commands/clean-oas.js"
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program.name("docblock-generator").description("Generate TSDoc doc-blocks")
|
||||
|
||||
// define common options
|
||||
const typeOption = new Option("--type <type>", "The type of docs to generate.")
|
||||
.choices(["all", "docs", "oas"])
|
||||
.default("all")
|
||||
|
||||
const generateExamplesOption = new Option(
|
||||
"--generate-examples",
|
||||
"Whether to generate examples"
|
||||
).default(false)
|
||||
|
||||
program
|
||||
.command("run")
|
||||
.description("Generate TSDoc doc-blocks for specified files.")
|
||||
@@ -18,17 +29,23 @@ program
|
||||
"--dry-run",
|
||||
"Whether to run the command without writing the changes."
|
||||
)
|
||||
.addOption(typeOption)
|
||||
.addOption(generateExamplesOption)
|
||||
.action(run)
|
||||
|
||||
program
|
||||
.command("run:changes")
|
||||
.description("Generate TSDoc doc-blocks for changed files in git.")
|
||||
.addOption(typeOption)
|
||||
.addOption(generateExamplesOption)
|
||||
.action(runGitChanges)
|
||||
|
||||
program
|
||||
.command("run:commit")
|
||||
.description("Generate TSDoc doc-blocks for changed files in a commit.")
|
||||
.argument("<commitSha>", "The SHA of a commit.")
|
||||
.addOption(typeOption)
|
||||
.addOption(generateExamplesOption)
|
||||
.action(runGitCommit)
|
||||
|
||||
program
|
||||
@@ -36,10 +53,19 @@ program
|
||||
.description(
|
||||
"Generate TSDoc doc-blocks for files part of the latest release. It will retrieve the files of commits between the latest two releases."
|
||||
)
|
||||
.addOption(typeOption)
|
||||
.addOption(generateExamplesOption)
|
||||
.option(
|
||||
"--tag <tag>",
|
||||
"Specify a release tag to use rather than the latest release."
|
||||
)
|
||||
.action(runRelease)
|
||||
|
||||
program
|
||||
.command("clean:oas")
|
||||
.description(
|
||||
"Check generated OAS under the `oas-output/operations` directory and remove any OAS that no longer exists."
|
||||
)
|
||||
.action(cleanOas)
|
||||
|
||||
program.parse()
|
||||
|
||||
30
docs-util/packages/docblock-generator/src/types/index.d.ts
vendored
Normal file
30
docs-util/packages/docblock-generator/src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
|
||||
declare type CodeSample = {
|
||||
lang: string
|
||||
label: string
|
||||
source: string
|
||||
}
|
||||
|
||||
export declare type OpenApiOperation = Partial<OpenAPIV3.OperationObject> & {
|
||||
"x-authenticated"?: boolean
|
||||
"x-codeSamples"?: CodeSample[]
|
||||
}
|
||||
|
||||
export declare type CommonCliOptions = {
|
||||
type: "all" | "oas" | "docs"
|
||||
generateExamples?: boolean
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export declare type OpenApiSchema = OpenAPIV3.SchemaObject & {
|
||||
"x-schemaName"?: string
|
||||
}
|
||||
|
||||
export declare interface OpenApiTagObject extends OpenAPIV3.TagObject {
|
||||
"x-associatedSchema"?: OpenAPIV3.ReferenceObject
|
||||
}
|
||||
|
||||
export declare interface OpenApiDocument extends OpenAPIV3.Document {
|
||||
tags?: OpenApiTagObject[]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { stringify } from "yaml"
|
||||
import { DOCBLOCK_END_LINE, DOCBLOCK_NEW_LINE } from "../constants.js"
|
||||
import { OpenApiOperation, OpenApiSchema } from "../types/index.js"
|
||||
|
||||
/**
|
||||
* Retrieve the OAS as a formatted string that can be used as a comment.
|
||||
*
|
||||
* @param oas - The OAS operation to format.
|
||||
* @param oasPrefix - The OAS prefix that's used before the OAS operation.
|
||||
* @returns The formatted OAS comment.
|
||||
*/
|
||||
export default function formatOas(
|
||||
oas: OpenApiOperation | OpenApiSchema,
|
||||
oasPrefix: string
|
||||
) {
|
||||
return `* ${oasPrefix}${DOCBLOCK_NEW_LINE}${stringify(oas).replaceAll(
|
||||
"\n",
|
||||
DOCBLOCK_NEW_LINE
|
||||
)}${DOCBLOCK_END_LINE}`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import path from "path"
|
||||
import getMonorepoRoot from "./get-monorepo-root.js"
|
||||
|
||||
export default function getOasOutputBasePath() {
|
||||
return path.join(getMonorepoRoot(), "docs-util", "oas-output")
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import ts from "typescript"
|
||||
|
||||
/**
|
||||
* Checks whether a node has comments.
|
||||
*
|
||||
* @param {ts.Node} node - The node to check.
|
||||
* @returns {boolean} Whether the node has comments.
|
||||
*/
|
||||
export default function nodeHasComments(node: ts.Node): boolean {
|
||||
return (
|
||||
ts.getLeadingCommentRanges(
|
||||
node.getSourceFile().getFullText(),
|
||||
node.getFullStart()
|
||||
) !== undefined
|
||||
)
|
||||
}
|
||||
42
docs-util/packages/docblock-generator/src/utils/parse-oas.ts
Normal file
42
docs-util/packages/docblock-generator/src/utils/parse-oas.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { parse } from "yaml"
|
||||
import { OpenApiOperation } from "../types/index.js"
|
||||
import { DOCBLOCK_LINE_ASTRIX } from "../constants.js"
|
||||
|
||||
export type ExistingOas = {
|
||||
oas: OpenApiOperation
|
||||
oasPrefix: string
|
||||
}
|
||||
|
||||
export default function parseOas(content: string): ExistingOas | undefined {
|
||||
content = content
|
||||
.replace(`/**\n`, "")
|
||||
.replaceAll(DOCBLOCK_LINE_ASTRIX, "")
|
||||
.replaceAll("*/", "")
|
||||
.trim()
|
||||
|
||||
if (!content.startsWith("@oas")) {
|
||||
// the file is of an invalid format.
|
||||
return
|
||||
}
|
||||
|
||||
// extract oas prefix line
|
||||
const splitNodeComments = content.split("\n")
|
||||
const oasPrefix = content.split("\n")[0]
|
||||
content = splitNodeComments.slice(1).join("\n")
|
||||
|
||||
let oas: OpenApiOperation | undefined
|
||||
|
||||
try {
|
||||
oas = parse(content) as OpenApiOperation
|
||||
} catch (e) {
|
||||
// couldn't parse the OAS, so consider it
|
||||
// not existent
|
||||
}
|
||||
|
||||
return oas
|
||||
? {
|
||||
oas,
|
||||
oasPrefix,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -2,6 +2,12 @@ export function capitalize(str: string): string {
|
||||
return `${str.charAt(0).toUpperCase()}${str.substring(1).toLowerCase()}`
|
||||
}
|
||||
|
||||
export function wordsToCamel(str: string): string {
|
||||
return `${str.charAt(0).toLowerCase()}${str
|
||||
.substring(1)
|
||||
.replaceAll(/\s([a-zA-Z])/g, (captured) => captured.toUpperCase())}`
|
||||
}
|
||||
|
||||
export function camelToWords(str: string): string {
|
||||
return str
|
||||
.replaceAll(/([A-Z])/g, " $1")
|
||||
@@ -23,6 +29,39 @@ export function snakeToWords(str: string): string {
|
||||
return str.replaceAll("_", " ").toLowerCase()
|
||||
}
|
||||
|
||||
export function kebabToTitle(str: string): string {
|
||||
return str
|
||||
.split("-")
|
||||
.map((word) => capitalize(word))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
export function kebabToCamel(str: string): string {
|
||||
return str
|
||||
.split("-")
|
||||
.map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word
|
||||
}
|
||||
return `${word.charAt(0).toUpperCase()}${word.substring(1)}`
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function wordsToKebab(str: string): string {
|
||||
return str
|
||||
.split(" ")
|
||||
.map((word) => word.toLowerCase())
|
||||
.join("-")
|
||||
}
|
||||
|
||||
export function wordsToPascal(str: string): string {
|
||||
return str
|
||||
.split(" ")
|
||||
.map((word) => capitalize(word))
|
||||
.join("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove parts of the name such as DTO, Filterable, etc...
|
||||
*
|
||||
|
||||
@@ -450,6 +450,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@faker-js/faker@npm:^8.4.0":
|
||||
version: 8.4.0
|
||||
resolution: "@faker-js/faker@npm:8.4.0"
|
||||
checksum: 2dd9a3f6a38a70baa8d8ae222d9d371eea2b56eba4bd12bb138230e9481687686330fddee7581d3f285cf8ea8fe1534e08e43a110de8743e561a6eccc3fdc670
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fastify/busboy@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "@fastify/busboy@npm:2.1.0"
|
||||
@@ -2213,15 +2220,20 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "docblock-generator@workspace:packages/docblock-generator"
|
||||
dependencies:
|
||||
"@faker-js/faker": ^8.4.0
|
||||
"@octokit/core": ^5.0.2
|
||||
"@types/node": ^20.9.4
|
||||
commander: ^11.1.0
|
||||
dotenv: ^16.3.1
|
||||
eslint: ^8.56.0
|
||||
minimatch: ^9.0.3
|
||||
openapi-types: ^12.1.3
|
||||
pluralize: ^8.0.0
|
||||
prettier: ^3.2.4
|
||||
ts-node: ^10.9.1
|
||||
typescript: 5.2
|
||||
utils: "*"
|
||||
yaml: ^2.3.4
|
||||
bin:
|
||||
workflow-diagrams-generator: dist/index.js
|
||||
languageName: unknown
|
||||
@@ -3863,6 +3875,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-types@npm:^12.1.3":
|
||||
version: 12.1.3
|
||||
resolution: "openapi-types@npm:12.1.3"
|
||||
checksum: 4ad4eb91ea834c237edfa6ab31394e87e00c888fc2918009763389c00d02342345195d6f302d61c3fd807f17723cd48df29b47b538b68375b3827b3758cd520f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"optionator@npm:^0.9.3":
|
||||
version: 0.9.3
|
||||
resolution: "optionator@npm:0.9.3"
|
||||
@@ -4127,6 +4146,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pluralize@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "pluralize@npm:8.0.0"
|
||||
checksum: 2044cfc34b2e8c88b73379ea4a36fc577db04f651c2909041b054c981cd863dd5373ebd030123ab058d194ae615d3a97cfdac653991e499d10caf592e8b3dc33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pony-cause@npm:^2.1.2":
|
||||
version: 2.1.10
|
||||
resolution: "pony-cause@npm:2.1.10"
|
||||
@@ -4189,6 +4215,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:^3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "prettier@npm:3.2.4"
|
||||
bin:
|
||||
prettier: bin/prettier.cjs
|
||||
checksum: 88dfeb78ac6096522c9a5b81f1413d875f568420d9bb6a5e5103527912519b993f2bcdcac311fcff5718d5869671d44e4f85827d3626f3a6ce32b9abc65d88e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"process-nextick-args@npm:~2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "process-nextick-args@npm:2.0.1"
|
||||
@@ -5441,7 +5476,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.3.3":
|
||||
"yaml@npm:^2.3.3, yaml@npm:^2.3.4":
|
||||
version: 2.3.4
|
||||
resolution: "yaml@npm:2.3.4"
|
||||
checksum: cf03b68f8fef5e8516b0f0b54edaf2459f1648317fc6210391cf606d247e678b449382f4bd01f77392538429e306c7cba8ff46ff6b37cac4de9a76aff33bd9e1
|
||||
|
||||
@@ -119,19 +119,19 @@ export type QueryConfig<TEntity extends BaseEntity> = {
|
||||
*/
|
||||
export type RequestQueryFields = {
|
||||
/**
|
||||
* {@inheritDoc FindParams.expand}
|
||||
* Comma-separated relations that should be expanded in the returned data.
|
||||
*/
|
||||
expand?: string
|
||||
/**
|
||||
* {@inheritDoc FindParams.fields}
|
||||
* Comma-separated fields that should be included in the returned data.
|
||||
*/
|
||||
fields?: string
|
||||
/**
|
||||
* {@inheritDoc FindPaginationParams.offset}
|
||||
* The number of items to skip when retrieving a list.
|
||||
*/
|
||||
offset?: number
|
||||
/**
|
||||
* {@inheritDoc FindPaginationParams.limit}
|
||||
* Limit the number of items returned in the list.
|
||||
*/
|
||||
limit?: number
|
||||
/**
|
||||
@@ -510,14 +510,14 @@ export class AddressCreatePayload {
|
||||
*/
|
||||
export class FindParams {
|
||||
/**
|
||||
* Comma-separated relations that should be expanded in the returned data.
|
||||
* {@inheritDoc RequestQueryFields.expand}
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
/**
|
||||
* Comma-separated fields that should be included in the returned data.
|
||||
* {@inheritDoc RequestQueryFields.fields}
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@@ -529,7 +529,7 @@ export class FindParams {
|
||||
*/
|
||||
export class FindPaginationParams {
|
||||
/**
|
||||
* The number of items to skip when retrieving a list.
|
||||
* {@inheritDoc RequestQueryFields.offset}
|
||||
* @defaultValue 0
|
||||
*/
|
||||
@IsNumber()
|
||||
@@ -538,7 +538,7 @@ export class FindPaginationParams {
|
||||
offset?: number = 0
|
||||
|
||||
/**
|
||||
* Limit the number of items returned in the list.
|
||||
* {@inheritDoc RequestQueryFields.limit}
|
||||
* @defaultValue 20
|
||||
*/
|
||||
@IsNumber()
|
||||
|
||||
@@ -11,6 +11,16 @@ import execa from "execa"
|
||||
const medusaPackagePath = path.dirname(
|
||||
require.resolve("@medusajs/medusa/package.json")
|
||||
)
|
||||
/**
|
||||
* OAS output directory
|
||||
*
|
||||
* @privateRemark
|
||||
* This should be the only directory OAS is loaded from for Medusa V2.
|
||||
* For now, we only use it if the --v2 flag it passed to the CLI tool.
|
||||
*/
|
||||
const oasOutputPath = path.resolve(
|
||||
__dirname, "..", "..", "..", "..", "docs-util", "oas-output"
|
||||
)
|
||||
const basePath = path.resolve(__dirname, `../../`)
|
||||
|
||||
export const runCLI = async (command: string, options: string[] = []) => {
|
||||
@@ -80,8 +90,8 @@ describe("command oas", () => {
|
||||
|
||||
it("generates oas using admin.oas.base.yaml", async () => {
|
||||
const yamlFilePath = path.resolve(
|
||||
medusaPackagePath,
|
||||
"oas",
|
||||
oasOutputPath,
|
||||
"base",
|
||||
"admin.oas.base.yaml"
|
||||
)
|
||||
const oasBase = (await readYaml(yamlFilePath)) as OpenAPIObject
|
||||
@@ -107,8 +117,8 @@ describe("command oas", () => {
|
||||
|
||||
it("generates oas using store.oas.base.yaml", async () => {
|
||||
const yamlFilePath = path.resolve(
|
||||
medusaPackagePath,
|
||||
"oas",
|
||||
oasOutputPath,
|
||||
"base",
|
||||
"store.oas.base.yaml"
|
||||
)
|
||||
const oasBase = (await readYaml(yamlFilePath)) as OpenAPIObject
|
||||
|
||||
@@ -26,6 +26,16 @@ const medusaTypesPath = path.dirname(
|
||||
const medusaUtilsPath = path.dirname(
|
||||
require.resolve("@medusajs/utils/package.json")
|
||||
)
|
||||
/**
|
||||
* OAS output directory
|
||||
*
|
||||
* @privateRemark
|
||||
* This should be the only directory OAS is loaded from for Medusa V2.
|
||||
* For now, we only use it if the --v2 flag it passed to the CLI tool.
|
||||
*/
|
||||
const oasOutputPath = path.resolve(
|
||||
__dirname, "..", "..", "..", "..", "docs-util", "oas-output"
|
||||
)
|
||||
const basePath = path.resolve(__dirname, "../")
|
||||
|
||||
/**
|
||||
@@ -53,6 +63,10 @@ export const commandOptions: Option[] = [
|
||||
"Custom base OAS file to use for swagger-inline."
|
||||
),
|
||||
new Option("-F, --force", "Ignore OAS validation and output OAS files."),
|
||||
new Option(
|
||||
"--v2",
|
||||
"Generate OAS files for V2 endpoints. This loads OAS from docs-util/oas-output/operations directory"
|
||||
)
|
||||
]
|
||||
|
||||
export function getCommand() {
|
||||
@@ -75,6 +89,7 @@ export async function execute(cliParams: OptionValues) {
|
||||
*/
|
||||
const dryRun = !!cliParams.dryRun
|
||||
const force = !!cliParams.force
|
||||
const v2 = !!cliParams.v2
|
||||
|
||||
const apiType: ApiType = cliParams.type
|
||||
|
||||
@@ -107,11 +122,11 @@ export async function execute(cliParams: OptionValues) {
|
||||
console.log(`🟣 Generating OAS - ${apiType}`)
|
||||
|
||||
if (apiType === "combined") {
|
||||
const adminOAS = await getOASFromCodebase("admin")
|
||||
const storeOAS = await getOASFromCodebase("store")
|
||||
const adminOAS = await getOASFromCodebase("admin", undefined, v2)
|
||||
const storeOAS = await getOASFromCodebase("store", undefined, v2)
|
||||
oas = await combineOAS(adminOAS, storeOAS)
|
||||
} else {
|
||||
oas = await getOASFromCodebase(apiType)
|
||||
oas = await getOASFromCodebase(apiType, undefined, v2)
|
||||
}
|
||||
|
||||
if (additionalPaths.length || baseFile) {
|
||||
@@ -137,10 +152,17 @@ export async function execute(cliParams: OptionValues) {
|
||||
*/
|
||||
async function getOASFromCodebase(
|
||||
apiType: ApiType,
|
||||
customBaseFile?: string
|
||||
customBaseFile?: string,
|
||||
v2?: boolean
|
||||
): Promise<OpenAPIObject> {
|
||||
const gen = await swaggerInline(
|
||||
[
|
||||
v2 ? [
|
||||
path.resolve(oasOutputPath, "operations", apiType),
|
||||
path.resolve(oasOutputPath, "schemas"),
|
||||
// We currently load error schemas from here. If we change
|
||||
// that in the future, we should change the path.
|
||||
path.resolve(medusaPackagePath, "dist", "api/middlewares"),
|
||||
] : [
|
||||
path.resolve(medusaTypesPath, "dist"),
|
||||
path.resolve(medusaUtilsPath, "dist"),
|
||||
path.resolve(medusaPackagePath, "dist", "models"),
|
||||
@@ -151,7 +173,7 @@ async function getOASFromCodebase(
|
||||
{
|
||||
base:
|
||||
customBaseFile ??
|
||||
path.resolve(medusaPackagePath, "oas", `${apiType}.oas.base.yaml`),
|
||||
path.resolve(oasOutputPath, v2 ? "base-v2" : "base", `${apiType}.oas.base.yaml`),
|
||||
format: ".json",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ const execa = require("execa")
|
||||
|
||||
const isDryRun = process.argv.indexOf("--dry-run") !== -1
|
||||
const withFullFile = process.argv.indexOf("--with-full-file") !== -1
|
||||
const v2 = process.argv.indexOf("--v2") !== -1
|
||||
const basePath = path.resolve(__dirname, `../`)
|
||||
const repoRootPath = path.resolve(basePath, `../../../`)
|
||||
const docsApiPath = path.resolve(repoRootPath, "www/apps/api-reference/specs")
|
||||
@@ -22,9 +23,13 @@ const run = async () => {
|
||||
}
|
||||
|
||||
const generateOASSource = async (outDir, apiType) => {
|
||||
const commandParams = ["oas", `--type=${apiType}`, `--out-dir=${outDir}`]
|
||||
if (v2) {
|
||||
commandParams.push(`--v2`)
|
||||
}
|
||||
const { all: logs } = await execa(
|
||||
"medusa-oas",
|
||||
["oas", `--type=${apiType}`, `--out-dir=${outDir}`],
|
||||
commandParams,
|
||||
{ cwd: basePath, all: true }
|
||||
)
|
||||
console.log(logs)
|
||||
|
||||
Reference in New Issue
Block a user