## 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.
287 lines
12 KiB
YAML
287 lines
12 KiB
YAML
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. |