From cf760965379534d7f4091a49a1dae68b612ad909 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 25 Oct 2025 10:15:16 +0100 Subject: [PATCH] fix: openapi models for feature endpoints --- AGENTS.md | 4 + CLAUDE.md | 2 + .../features/handlers/handleDeleteFeature.ts | 2 +- shared/api/_prevVersions/openapi1.2.0.ts | 456 ++++++++++++++++++ shared/api/balances/updateBalanceModels.ts | 25 + shared/api/balances/usageModels.ts | 22 + shared/api/common/customerData.ts | 48 +- shared/api/common/jsDocs.ts | 34 +- shared/api/core/coreOpenApi.ts | 41 +- shared/api/customers/customerOpModels.ts | 38 -- shared/api/customers/customersOpenApi.ts | 2 +- shared/api/features/apiFeature.ts | 46 +- shared/api/features/featureOpModels.ts | 79 ++- shared/api/features/featuresOpenApi.ts | 32 +- shared/api/openapi.ts | 197 ++++---- shared/api/products/productsOpenApi.ts | 12 +- 16 files changed, 770 insertions(+), 270 deletions(-) create mode 100644 shared/api/_prevVersions/openapi1.2.0.ts create mode 100644 shared/api/balances/updateBalanceModels.ts create mode 100644 shared/api/balances/usageModels.ts diff --git a/AGENTS.md b/AGENTS.md index 3fc38f0ae..ebe63da3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,10 @@ - This codebase uses Bun as its preferred package manager and Node runtime. +- **ALWAYS import from `zod/v4`**, not from `zod` directly. Example: `import { z } from "zod/v4";` + +- **ALWAYS use `.meta()` for zod-openapi schema registration**, NOT `.openapi()`. Example: `ApiProductSchema.meta({ id: "Product" })` + - Always prefer foo({ bar }) over foo(bar) method signatures - no matter if we are using only one argument or not, object as param are always better, as in the future when wanting to change the order of parameters, or add new ones - its easier. - When creating "hooks" folders, don't nest them under "components" diff --git a/CLAUDE.md b/CLAUDE.md index 8a3c41362..e28abf509 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,8 @@ - Prefer Guard clauses "if(!admin) return;" over explicity "if(admin) do X;" Early returns are better - Do not run "npx tsc" - run "tsc" instead as it is installed globally. + +- **ALWAYS use `.meta()` for zod-openapi schema registration**, NOT `.openapi()`. Example: `ApiProductSchema.meta({ id: "Product" })` ## Error Handling in API Routes - NEVER use `c.json({ message: "...", code: "..." }, statusCode)` pattern for input validation or expected errors in Hono routes - ALWAYS throw `RecaseError` from `@autumn/shared` for all validation errors, not found errors, forbidden errors, etc. diff --git a/server/src/internal/features/handlers/handleDeleteFeature.ts b/server/src/internal/features/handlers/handleDeleteFeature.ts index e70171dee..7e76555c7 100644 --- a/server/src/internal/features/handlers/handleDeleteFeature.ts +++ b/server/src/internal/features/handlers/handleDeleteFeature.ts @@ -62,6 +62,6 @@ export const handleDeleteFeature = async (req: any, res: any) => env: req.env, }); - res.status(200).json({ message: "Feature deleted" }); + res.status(200).json({ success: true }); }, }); diff --git a/shared/api/_prevVersions/openapi1.2.0.ts b/shared/api/_prevVersions/openapi1.2.0.ts new file mode 100644 index 000000000..4beb5b4eb --- /dev/null +++ b/shared/api/_prevVersions/openapi1.2.0.ts @@ -0,0 +1,456 @@ +import { writeFileSync } from "node:fs"; +import { + getListResponseSchema, + SuccessResponseSchema, +} from "@api/common/commonResponses.js"; +import yaml from "yaml"; +import { z } from "zod/v4"; +import { createDocument, type ZodOpenApiPathsObject } from "zod-openapi"; +import { UpdateBalancesParamsSchema } from "../balances/updateBalanceModels.js"; +import { SetUsageParamsSchema } from "../balances/usageModels.js"; +import { CustomerDataSchema } from "../common/customerData.js"; +import { EntityDataSchema } from "../common/entityData.js"; +import { setUsageJsDoc } from "../common/jsDocs.js"; +import { ApiEntityWithMeta } from "../entities/entitiesOpenApi.js"; +import { + ApiCusFeatureSchema, + ApiCusProductSchema, + ApiCustomerSchema, + ApiFeatureSchema, + ApiProductItemSchema, + CreateCustomerParamsSchema, + CreateCustomerQuerySchema, + CreateFeatureParamsSchema, + FEATURE_EXAMPLE, + GetCustomerQuerySchema, + ListCustomersResponseSchema, + UpdateCustomerParamsSchema, + UpdateFeatureParamsSchema, +} from "../models.js"; +import { ApiProductSchema, PRODUCT_EXAMPLE } from "../products/apiProduct.js"; +import { + CreateProductV2ParamsSchema, + UpdateProductV2ParamsSchema, +} from "../products/productOpModels.js"; + +// Register schema with .meta() for OpenAPI spec generation +export const ApiProductWithMeta = ApiProductSchema.meta({ + id: "Product", + examples: [PRODUCT_EXAMPLE], +}); + +// Register the schema with .meta() for OpenAPI spec generation +const ApiFeatureWithMeta = ApiFeatureSchema.extend({ + type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"]), +}).meta({ + id: "Feature", + examples: [FEATURE_EXAMPLE], +}); + +// Register schema with .meta() for OpenAPI spec generation +const ApiCustomerWithMeta = ApiCustomerSchema.meta({ + id: "Customer", +}); + +const productOps = { + "/products": { + get: { + summary: "List Products", + tags: ["products"], + responses: { + "200": { + description: "", + content: { + "application/json": { + schema: z.object({ + list: z.array(ApiProductWithMeta), + }), + }, + }, + }, + }, + }, + post: { + summary: "Create Product", + tags: ["products"], + requestBody: { + content: { + "application/json": { schema: CreateProductV2ParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiProductWithMeta } }, + }, + }, + }, + }, + "/products/{product_id}": { + get: { + summary: "Get Product", + tags: ["products"], + requestParams: { + path: z.object({ + product_id: z.string(), + }), + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiProductWithMeta } }, + }, + }, + }, + post: { + summary: "Update Product", + tags: ["products"], + requestParams: { + path: z.object({ + product_id: z.string(), + }), + }, + requestBody: { + content: { + "application/json": { schema: UpdateProductV2ParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiProductWithMeta } }, + }, + }, + }, + delete: { + summary: "Delete Product", + tags: ["products"], + requestParams: { + path: z.object({ + product_id: z.string(), + }), + query: z.object({ + all_versions: z.boolean().optional(), + }), + }, + responses: { + "200": { + description: "", + content: { + "application/json": { + schema: SuccessResponseSchema, + }, + }, + }, + }, + }, + }, +}; + +const featureOps = { + "/features": { + get: { + summary: "List Features", + tags: ["features"], + responses: { + "200": { + description: "", + content: { + "application/json": { + schema: getListResponseSchema({ schema: ApiFeatureWithMeta }), + }, + }, + }, + }, + }, + post: { + summary: "Create Feature", + tags: ["features"], + requestBody: { + content: { + "application/json": { schema: CreateFeatureParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiFeatureWithMeta } }, + }, + }, + }, + }, + "/features/{feature_id}": { + get: { + summary: "Get Feature", + tags: ["features"], + requestParams: { + path: z.object({ + feature_id: z.string(), + }), + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiFeatureWithMeta } }, + }, + }, + }, + post: { + summary: "Update Feature", + tags: ["features"], + requestParams: { + path: z.object({ + feature_id: z.string(), + }), + }, + requestBody: { + content: { + "application/json": { schema: UpdateFeatureParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: ApiFeatureWithMeta } }, + }, + }, + }, + delete: { + summary: "Delete Feature", + tags: ["features"], + requestParams: { + path: z.object({ + feature_id: z.string(), + }), + }, + responses: { + "200": { + description: "", + content: { + "application/json": { + schema: SuccessResponseSchema, + }, + }, + }, + }, + }, + }, +}; + +const customerOps = { + "/customers": { + get: { + summary: "List Customers", + tags: ["customers"], + requestParams: { + query: z.object({ + limit: z.number().int().min(10).max(100).optional(), + offset: z.number().int().min(0).optional(), + }), + }, + responses: { + "200": { + description: "200 OK", + content: { + "application/json": { schema: ListCustomersResponseSchema }, + }, + }, + }, + }, + post: { + summary: "Create Customer", + tags: ["customers"], + requestParams: { + query: CreateCustomerQuerySchema, + }, + requestBody: { + content: { + "application/json": { schema: CreateCustomerParamsSchema }, + }, + }, + responses: { + "200": { + description: "200 OK", + content: { "application/json": { schema: ApiCustomerWithMeta } }, + }, + }, + }, + }, + "/customers/{customer_id}": { + get: { + summary: "Get Customer", + tags: ["customers"], + requestParams: { + path: z.object({ + customer_id: z.string(), + }), + query: GetCustomerQuerySchema, + }, + responses: { + "200": { + description: "200 OK", + content: { "application/json": { schema: ApiCustomerWithMeta } }, + }, + }, + }, + post: { + summary: "Update Customer", + tags: ["customers"], + requestParams: { + path: z.object({ + customer_id: z.string(), + }), + query: z.object({ + expand: z.string().optional(), + }), + }, + requestBody: { + content: { + "application/json": { schema: UpdateCustomerParamsSchema }, + }, + }, + responses: { + "200": { + description: "200 OK", + content: { "application/json": { schema: ApiCustomerWithMeta } }, + }, + }, + }, + delete: { + summary: "Delete Customer", + tags: ["customers"], + requestParams: { + path: z.object({ + customer_id: z.string(), + }), + }, + responses: { + "200": { + description: "200 OK", + content: { + "application/json": { + schema: SuccessResponseSchema, + }, + }, + }, + }, + }, + }, + "/customers/{customer_id}/balances": { + post: { + summary: "Set Feature Balances", + description: "Set the balance of a feature for a specific customer", + tags: ["customers"], + requestParams: { + path: z.object({ + customer_id: z.string(), + }), + }, + requestBody: { + content: { + "application/json": { schema: UpdateBalancesParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { + "application/json": { schema: SuccessResponseSchema }, + }, + }, + }, + }, + }, +}; + +const coreOps: ZodOpenApiPathsObject = { + "/usage": { + post: { + summary: "Set Usage", + description: setUsageJsDoc, + tags: ["core"], + requestBody: { + content: { + "application/json": { schema: SetUsageParamsSchema }, + }, + }, + responses: { + "200": { + description: "", + content: { "application/json": { schema: SuccessResponseSchema } }, + }, + }, + }, + }, +}; + +const OPENAPI_1_2_0 = createDocument( + { + openapi: "3.1.0", + info: { + title: "Autumn API", + version: "1.2.0", + }, + + servers: [ + { + url: "https://api.useautumn.com", + description: "Production server", + }, + ], + + security: [ + { + secretKey: [], + }, + ], + components: { + schemas: { + CustomerData: CustomerDataSchema, + EntityData: EntityDataSchema.meta({ + id: "EntityData", + description: "Entity data for creating an entity", + }), + Customer: ApiCustomerWithMeta, + CustomerProduct: ApiCusProductSchema, + CustomerFeature: ApiCusFeatureSchema.meta({ + id: "CustomerFeature", + description: "Customer feature object returned by the API", + }), + Product: ApiProductWithMeta, + ProductItem: ApiProductItemSchema, + Feature: ApiFeatureWithMeta, + Entity: ApiEntityWithMeta, + }, + securitySchemes: { + secretKey: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + + paths: { + ...productOps, + ...featureOps, + ...customerOps, + ...coreOps, + }, + }, + { + // Disable the "Output" suffix that zod-openapi adds to response schemas + outputIdSuffix: "", + }, +); + +export const writeOpenApi_1_2_0 = () => { + const yamlContent = yaml.stringify( + JSON.parse(JSON.stringify(OPENAPI_1_2_0, null, 2)), + ); + writeFileSync( + `${process.env.STAINLESS_PATH?.replace("\\ ", " ")}/openapi.yml`, + yamlContent, + "utf8", + ); +}; diff --git a/shared/api/balances/updateBalanceModels.ts b/shared/api/balances/updateBalanceModels.ts new file mode 100644 index 000000000..a630714c5 --- /dev/null +++ b/shared/api/balances/updateBalanceModels.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; + +export const UpdateBalancesParamsSchema = z + .object({ + balances: z.array( + z.object({ + feature_id: z.string().meta({ + description: "The ID of the feature to update balance for.", + }), + balance: z.number().meta({ + description: "The new balance value.", + }), + }), + ), + }) + .meta({ + example: { + balances: [ + { + feature_id: "tokens", + balance: 1000, + }, + ], + }, + }); diff --git a/shared/api/balances/usageModels.ts b/shared/api/balances/usageModels.ts new file mode 100644 index 000000000..108c03c23 --- /dev/null +++ b/shared/api/balances/usageModels.ts @@ -0,0 +1,22 @@ +import { z } from "zod/v4"; +import { CustomerDataSchema } from "../common/customerData.js"; +export const SetUsageParamsSchema = z.object({ + customer_id: z.string().nonempty().meta({ + description: "The ID of the customer.", + }), + + feature_id: z.string().meta({ + description: "The ID of the feature to set usage for.", + }), + + value: z.number().meta({ + description: + "The value you want to set this customer's usage of the feature to.", + }), + + entity_id: z.string().optional().meta({ + description: "The ID of the entity to set usage for.", + }), + + customer_data: CustomerDataSchema.optional(), +}); diff --git a/shared/api/common/customerData.ts b/shared/api/common/customerData.ts index 28811e060..fbf050261 100644 --- a/shared/api/common/customerData.ts +++ b/shared/api/common/customerData.ts @@ -2,31 +2,29 @@ import { z } from "zod/v4"; // Base schema without top-level .meta() to avoid side effects during imports // Individual field descriptions are kept as they don't cause registry conflicts -export const CustomerDataSchema = z.object({ - name: z.string().nullish().meta({ - description: "Customer's name", - example: "John Doe", - }), - email: z.string().nullish().meta({ - description: "Customer's email address", - example: "john@example.com", - }), - fingerprint: z.string().nullish().meta({ - description: - "Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse", - example: "fp_123abc", - }), - metadata: z - .record(z.any(), z.any()) - .nullish() - .meta({ - description: "Additional metadata for the customer", - example: { company: "Acme Inc" }, +export const CustomerDataSchema = z + .object({ + name: z.string().nullish().meta({ + description: "Customer's name", }), - stripe_id: z.string().nullish().meta({ - description: "Stripe customer ID if you already have one", - example: "cus_stripe123", - }), -}); + email: z.string().nullish().meta({ + description: "Customer's email address", + }), + fingerprint: z.string().nullish().meta({ + description: + "Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse", + }), + metadata: z.record(z.any(), z.any()).nullish().meta({ + description: "Additional metadata for the customer", + }), + stripe_id: z.string().nullish().meta({ + description: "Stripe customer ID if you already have one", + }), + }) + .meta({ + id: "CustomerData", + description: + "Used to add customer details like name or email when auto-creating a customer.", + }); export type CustomerData = z.infer; diff --git a/shared/api/common/jsDocs.ts b/shared/api/common/jsDocs.ts index 1660239de..cf4193f43 100644 --- a/shared/api/common/jsDocs.ts +++ b/shared/api/common/jsDocs.ts @@ -1,5 +1,10 @@ import { ExtAttachBodySchema, ExtCheckoutParamsSchema } from "@api/models.js"; -import { createJSDocDescription, docLink, example } from "@api/utils/openApiHelpers.js"; +import { + createJSDocDescription, + docLink, + example, +} from "@api/utils/openApiHelpers.js"; +import { SetUsageParamsSchema } from "../balances/usageModels.js"; import { CheckParamsSchema } from "../core/checkModels.js"; import { BillingPortalParamsSchema, @@ -104,8 +109,7 @@ export const trackJsDoc = createJSDocDescription({ }); export const cancelJsDoc = createJSDocDescription({ - description: - "Cancel a customer's subscription to a product.", + description: "Cancel a customer's subscription to a product.", whenToUse: "Use this when a customer wants to stop their subscription. Supports immediate or end-of-period cancellation.", body: CancelBodySchema, @@ -199,3 +203,27 @@ export const queryJsDoc = createJSDocDescription({ }), ], }); + +export const setUsageJsDoc = createJSDocDescription({ + description: + "Set usage for a feature. This is similar to /track instead of incrementing usage, it sets the usage value to exactly what is provided.", + whenToUse: "Use this to set usage for a feature instead of incrementing it.", + body: SetUsageParamsSchema, + examples: [ + example({ + values: { + customer_id: "123", + feature_id: "api_calls", + value: 10000, + }, + description: "Set usage for a feature", + }), + ], + methodName: "usage", + docs: [ + docLink({ + url: "https://docs.useautumn.com/api-reference/core/usage", + title: "Set Usage", + }), + ], +}); diff --git a/shared/api/core/coreOpenApi.ts b/shared/api/core/coreOpenApi.ts index 8d2eea28e..296a36c82 100644 --- a/shared/api/core/coreOpenApi.ts +++ b/shared/api/core/coreOpenApi.ts @@ -5,6 +5,8 @@ import { ExtCheckoutParamsSchema, } from "@api/models.js"; import type { ZodOpenApiPathsObject } from "zod-openapi"; +import { SetUsageParamsSchema } from "../balances/usageModels.js"; +import { SuccessResponseSchema } from "../common/commonResponses.js"; import { attachJsDoc, billingPortalJsDoc, @@ -12,6 +14,7 @@ import { checkJsDoc, checkoutJsDoc, queryJsDoc, + setUsageJsDoc, setupPaymentJsDoc, trackJsDoc, } from "../common/jsDocs.js"; @@ -118,24 +121,7 @@ export const coreOps: ZodOpenApiPathsObject = { }, }, }, - "/usage": { - post: { - summary: "Set Usage", - description: "Set or increment usage for a metered feature. This is similar to /track but specifically for usage-based features with the set_usage flag enabled by default.", - tags: ["core"], - requestBody: { - content: { - "application/json": { schema: TrackParamsSchema }, - }, - }, - responses: { - "200": { - description: "200 OK", - content: { "application/json": { schema: TrackResultSchema } }, - }, - }, - }, - }, + "/query": { post: { summary: "Query Analytics", @@ -210,4 +196,23 @@ export const coreOps: ZodOpenApiPathsObject = { }, }, }, + + "/usage": { + post: { + summary: "Set Usage", + description: setUsageJsDoc, + tags: ["core"], + requestBody: { + content: { + "application/json": { schema: SetUsageParamsSchema }, + }, + }, + responses: { + "200": { + description: "200 OK", + content: { "application/json": { schema: SuccessResponseSchema } }, + }, + }, + }, + }, }; diff --git a/shared/api/customers/customerOpModels.ts b/shared/api/customers/customerOpModels.ts index b72e93500..75478440e 100644 --- a/shared/api/customers/customerOpModels.ts +++ b/shared/api/customers/customerOpModels.ts @@ -177,45 +177,7 @@ export const ListCustomersResponseSchema = z.object({ }), }); -// Update Balances Params (based on handleUpdateBalances logic) -export const UpdateBalancesParamsSchema = z.object({ - balances: z - .array( - z.object({ - feature_id: z.string().meta({ - description: "Feature ID to update balance for", - example: "api_calls", - }), - balance: z.number().optional().meta({ - description: "New balance value (required if unlimited is not true)", - example: 1000, - }), - unlimited: z.boolean().optional().meta({ - description: "Set to true to make the feature unlimited", - example: false, - }), - interval: z.string().optional().meta({ - description: - "Interval to match for balance update (e.g., 'month', 'year')", - example: "month", - }), - interval_count: z.number().optional().meta({ - description: "Interval count to match for balance update", - example: 1, - }), - entity_id: z.string().optional().meta({ - description: "Entity ID for entity-specific balance updates", - example: "entity_123", - }), - }), - ) - .meta({ - description: "Array of feature balances to update", - }), -}); - export type CreateCustomerParams = z.infer; export type UpdateCustomerParams = z.infer; export type ListCustomersQuery = z.infer; export type ListCustomersResponse = z.infer; -export type UpdateBalancesParams = z.infer; diff --git a/shared/api/customers/customersOpenApi.ts b/shared/api/customers/customersOpenApi.ts index b9dde80d2..925760566 100644 --- a/shared/api/customers/customersOpenApi.ts +++ b/shared/api/customers/customersOpenApi.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; +import { UpdateBalancesParamsSchema } from "../balances/updateBalanceModels.js"; import { SuccessResponseSchema } from "../common/commonResponses.js"; import { ApiCustomerSchema } from "./apiCustomer.js"; import { @@ -6,7 +7,6 @@ import { CreateCustomerQuerySchema, GetCustomerQuerySchema, ListCustomersResponseSchema, - UpdateBalancesParamsSchema, UpdateCustomerParamsSchema, } from "./customerOpModels.js"; diff --git a/shared/api/features/apiFeature.ts b/shared/api/features/apiFeature.ts index 9050c1f34..5e3f5ba2d 100644 --- a/shared/api/features/apiFeature.ts +++ b/shared/api/features/apiFeature.ts @@ -9,47 +9,63 @@ export enum ApiFeatureType { CreditSystem = "credit_system", } +export const FEATURE_EXAMPLE = { + id: "tokens", + name: "Tokens", + type: "single_use", + display: { + singular: "token", + plural: "tokens", + }, + credit_schema: null, + archived: false, +}; + // Base schema without .meta() to avoid side effects during imports export const ApiFeatureSchema = z.object({ id: z.string().meta({ - description: "The unique identifier of the feature", - example: "", + description: + "The ID of the feature, used to refer to it in other API calls like /track or /check.", }), name: z.string().nullish().meta({ - description: "The name of the feature", - example: "", + description: "The name of the feature.", }), type: z.enum(ApiFeatureType).meta({ description: "The type of the feature", - example: "", }), display: z .object({ - singular: z.string(), - plural: z.string(), + singular: z.string().meta({ + description: "The singular display name for the feature.", + }), + plural: z.string().meta({ + description: "The plural display name for the feature.", + }), }) .nullish() .meta({ - description: "Display names for the feature", - example: { singular: "", plural: "" }, + description: "Singular and plural display names for the feature.", }), credit_schema: z .array( z.object({ - metered_feature_id: z.string(), - credit_cost: z.number(), + metered_feature_id: z.string().meta({ + description: + "The ID of the metered feature (should be a single_use feature).", + }), + credit_cost: z.number().meta({ + description: "The credit cost of the metered feature.", + }), }), ) .nullish() .meta({ - description: "Credit cost schema for credit system features", - example: [{ metered_feature_id: "", credit_cost: 123 }], + description: "Credit cost schema for credit system features.", }), archived: z.boolean().nullish().meta({ - description: "Whether or not the feature is archived", - example: false, + description: "Whether or not the feature is archived.", }), }); diff --git a/shared/api/features/featureOpModels.ts b/shared/api/features/featureOpModels.ts index 10b6c41c8..d2221be42 100644 --- a/shared/api/features/featureOpModels.ts +++ b/shared/api/features/featureOpModels.ts @@ -1,30 +1,33 @@ import { z } from "zod/v4"; import { ApiFeatureType } from "./apiFeature.js"; +const featureDescriptions = { + id: "The ID of the feature. This is used to refer to it in other API calls like /track or /check.", + name: "The name of the feature.", + type: "The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system.", + display: + "Singular and plural display names for the feature in your user interface.", + + credit_schema: + "A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.", + + archived: + "Whether the feature is archived. Archived features are hidden from the dashboard and list features endpoint.", +}; + // Create Feature Params export const CreateFeatureParamsSchema = z.object({ - id: z.string().meta({ - description: "The ID of the feature", - example: "feature_123", - }), - name: z.string().nullish().meta({ - description: "The name of the feature", - example: "API Calls", - }), - type: z.enum(ApiFeatureType).meta({ - description: "The type of the feature", - example: "single_use", - }), + id: z.string().meta({ description: featureDescriptions.id }), + name: z.string().nullish().meta({ description: featureDescriptions.name }), + type: z.enum(ApiFeatureType).meta({ description: featureDescriptions.type }), display: z .object({ singular: z.string(), plural: z.string(), }) .nullish() - .meta({ - description: "Display names for the feature", - example: { singular: "API Call", plural: "API Calls" }, - }), + .meta({ description: featureDescriptions.display }), + credit_schema: z .array( z.object({ @@ -33,37 +36,25 @@ export const CreateFeatureParamsSchema = z.object({ }), ) .nullish() - .meta({ - description: - "Credit schema for credit system features (only applicable when type is credit_system)", - example: [{ metered_feature_id: "api_calls", credit_cost: 10 }], - }), + .meta({ description: featureDescriptions.credit_schema }), }); // Update Feature Params export const UpdateFeatureParamsSchema = z.object({ - id: z.string().optional().meta({ - description: "The ID of the feature", - example: "feature_123", - }), - name: z.string().optional().meta({ - description: "The name of the feature", - example: "API Calls", - }), - type: z.enum(ApiFeatureType).optional().meta({ - description: "The type of the feature", - example: "single_use", - }), + id: z.string().optional().meta({ description: featureDescriptions.id }), + name: z.string().optional().meta({ description: featureDescriptions.name }), + type: z + .enum(ApiFeatureType) + .optional() + .meta({ description: featureDescriptions.type }), display: z .object({ singular: z.string(), plural: z.string(), }) .optional() - .meta({ - description: "Display names for the feature", - example: { singular: "API Call", plural: "API Calls" }, - }), + .meta({ description: featureDescriptions.display }), + credit_schema: z .array( z.object({ @@ -72,15 +63,11 @@ export const UpdateFeatureParamsSchema = z.object({ }), ) .optional() - .meta({ - description: - "Credit schema for credit system features (only applicable when type is credit_system)", - example: [{ metered_feature_id: "api_calls", credit_cost: 10 }], - }), - archived: z.boolean().optional().meta({ - description: "Whether the feature is archived", - example: false, - }), + .meta({ description: featureDescriptions.credit_schema }), + archived: z + .boolean() + .optional() + .meta({ description: featureDescriptions.archived }), }); export type CreateFeatureParams = z.infer; diff --git a/shared/api/features/featuresOpenApi.ts b/shared/api/features/featuresOpenApi.ts index b330ef88c..342966e4b 100644 --- a/shared/api/features/featuresOpenApi.ts +++ b/shared/api/features/featuresOpenApi.ts @@ -3,16 +3,19 @@ import { getListResponseSchema, SuccessResponseSchema, } from "../common/commonResponses.js"; -import { ApiFeatureSchema } from "./apiFeature.js"; +import { ApiFeatureSchema, FEATURE_EXAMPLE } from "./apiFeature.js"; import { CreateFeatureParamsSchema, UpdateFeatureParamsSchema, } from "./featureOpModels.js"; // Register the schema with .meta() for OpenAPI spec generation -export const ApiFeatureWithMeta = ApiFeatureSchema.meta({ +export const ApiFeatureWithMeta = ApiFeatureSchema.extend({ + type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"]), +}).meta({ id: "Feature", - description: "Feature object returned by the API", + description: "", + example: FEATURE_EXAMPLE, }); export const featureOps = { @@ -20,14 +23,9 @@ export const featureOps = { get: { summary: "List Features", tags: ["features"], - requestParams: { - query: z.object({ - include_archived: z.boolean().optional(), - }), - }, responses: { "200": { - description: "200 OK", + description: "", content: { "application/json": { schema: getListResponseSchema({ schema: ApiFeatureWithMeta }), @@ -46,24 +44,24 @@ export const featureOps = { }, responses: { "200": { - description: "200 OK", + description: "", content: { "application/json": { schema: ApiFeatureWithMeta } }, }, }, }, }, - "/features/{featureId}": { + "/features/{feature_id}": { get: { summary: "Get Feature", tags: ["features"], requestParams: { path: z.object({ - featureId: z.string(), + feature_id: z.string(), }), }, responses: { "200": { - description: "200 OK", + description: "", content: { "application/json": { schema: ApiFeatureWithMeta } }, }, }, @@ -73,7 +71,7 @@ export const featureOps = { tags: ["features"], requestParams: { path: z.object({ - featureId: z.string(), + feature_id: z.string(), }), }, requestBody: { @@ -83,7 +81,7 @@ export const featureOps = { }, responses: { "200": { - description: "200 OK", + description: "", content: { "application/json": { schema: ApiFeatureWithMeta } }, }, }, @@ -93,12 +91,12 @@ export const featureOps = { tags: ["features"], requestParams: { path: z.object({ - featureId: z.string(), + feature_id: z.string(), }), }, responses: { "200": { - description: "200 OK", + description: "", content: { "application/json": { schema: SuccessResponseSchema, diff --git a/shared/api/openapi.ts b/shared/api/openapi.ts index 3dbcd4d46..8b93bb967 100644 --- a/shared/api/openapi.ts +++ b/shared/api/openapi.ts @@ -1,120 +1,117 @@ -import "dotenv/config"; +// import "dotenv/config"; +// import { execSync } from "node:child_process"; +// import { existsSync, writeFileSync } from "node:fs"; +// import yaml from "yaml"; +// import { createDocument } from "zod-openapi"; +// import { OPENAPI_1_2_0 } from "./_prevVersions/openapi1.2.0.js"; +// import { CustomerDataSchema } from "./common/customerData.js"; +// import { EntityDataSchema } from "./common/entityData.js"; +// import { coreOps } from "./core/coreOpenApi.js"; +// import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js"; +// import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js"; +// import { +// ApiCustomerWithMeta, +// customerOps, +// } from "./customers/customersOpenApi.js"; +// import { ApiEntityWithMeta } from "./entities/entitiesOpenApi.js"; +// import { ApiFeatureWithMeta, featureOps } from "./features/featuresOpenApi.js"; +// import { ApiProductItemSchema } from "./products/apiProductItem.js"; +// import { ApiProductWithMeta, productOps } from "./products/productsOpenApi.js"; + +// const API_VERSION = "1.2.0"; + +// const document = createDocument( +// { +// openapi: "3.1.0", +// info: { +// title: "Autumn API", +// version: API_VERSION, +// }, + +// servers: [ +// { +// url: "https://api.useautumn.com", +// description: "Production server", +// }, +// ], + +// security: [ +// { +// secretKey: [], +// }, +// ], +// components: { +// schemas: { +// CustomerData: CustomerDataSchema, +// EntityData: EntityDataSchema.meta({ +// id: "EntityData", +// description: "Entity data for creating an entity", +// }), +// Customer: ApiCustomerWithMeta, +// CustomerProduct: ApiCusProductSchema, +// CustomerFeature: ApiCusFeatureSchema.meta({ +// id: "CustomerFeature", +// description: "Customer feature object returned by the API", +// }), +// Product: ApiProductWithMeta, +// ProductItem: ApiProductItemSchema, +// Feature: ApiFeatureWithMeta, +// Entity: ApiEntityWithMeta, +// }, +// securitySchemes: { +// secretKey: { +// type: "http", +// scheme: "bearer", +// bearerFormat: "JWT", +// }, +// }, +// }, + +// paths: { +// ...coreOps, +// ...customerOps, +// ...productOps, +// ...featureOps, +// // ...entityOps, +// // ...referralOps, +// }, +// }, +// { +// // Disable the "Output" suffix that zod-openapi adds to response schemas +// outputIdSuffix: "", +// }, +// ); + import { execSync } from "node:child_process"; -import { existsSync, writeFileSync } from "node:fs"; -import yaml from "yaml"; -import { createDocument } from "zod-openapi"; -import { CustomerDataSchema } from "./common/customerData.js"; -import { EntityDataSchema } from "./common/entityData.js"; -import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js"; -import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js"; -import { ApiCustomerWithMeta } from "./customers/customersOpenApi.js"; -import { ApiEntityWithMeta } from "./entities/entitiesOpenApi.js"; -import { ApiFeatureWithMeta } from "./features/featuresOpenApi.js"; -import { ApiProductItemSchema } from "./products/apiProductItem.js"; -import { ApiProductWithMeta, productOps } from "./products/productsOpenApi.js"; +import { existsSync } from "node:fs"; +import { writeOpenApi_1_2_0 } from "./_prevVersions/openapi1.2.0.js"; -const API_VERSION = "1.2.0"; - -const document = createDocument( - { - openapi: "3.1.0", - info: { - title: "Autumn API", - version: API_VERSION, - }, - - servers: [ - { - url: "https://api.useautumn.com", - description: "Production server", - }, - ], - - security: [ - { - secretKey: [], - }, - ], - components: { - schemas: { - CustomerData: CustomerDataSchema.meta({ - id: "CustomerData", - description: "Customer data for creating or updating a customer", - }), - EntityData: EntityDataSchema.meta({ - id: "EntityData", - description: "Entity data for creating an entity", - }), - Customer: ApiCustomerWithMeta, - CustomerProduct: ApiCusProductSchema, - CustomerFeature: ApiCusFeatureSchema.meta({ - id: "CustomerFeature", - description: "Customer feature object returned by the API", - }), - Product: ApiProductWithMeta, - ProductItem: ApiProductItemSchema, - Feature: ApiFeatureWithMeta, - Entity: ApiEntityWithMeta, - }, - securitySchemes: { - secretKey: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - }, - }, - }, - - paths: { - ...productOps, - // ...coreOps, - // ...featureOps, - // ...customerOps, - // ...entityOps, - // ...referralOps, - }, - }, - { - // Disable the "Output" suffix that zod-openapi adds to response schemas - outputIdSuffix: "", - }, -); +// import { OPENAPI_1_2_0 } from "./_prevVersions/openapi1.2.0.js"; // Export to YAML file during build if (process.env.NODE_ENV !== "production") { try { // If --no-build flag is present, return after writing openapi.yml if (process.argv.includes("--no-build")) { - const yamlContent = yaml.stringify( - JSON.parse(JSON.stringify(document, null, 2)), - ); - writeFileSync( - `${process.env.STAINLESS_PATH?.replace("\\ ", " ")}/openapi.yml`, - yamlContent, - "utf8", - ); + writeOpenApi_1_2_0(); console.log( `OpenAPI document exported to ${process.env.STAINLESS_PATH}/openapi.yml`, ); process.exit(0); } // Convert to JSON first to strip out Zod schemas and function references - const jsonStr = JSON.stringify(document, null, 2); - - // Convert JSON to YAML (this avoids function serialization issues) - const jsonObj = JSON.parse(jsonStr); - const yamlContent = yaml.stringify(jsonObj); if (process.env.STAINLESS_PATH) { - writeFileSync( - `${process.env.STAINLESS_PATH.replace("\\ ", " ")}/openapi.yml`, - yamlContent, - "utf8", - ); + writeOpenApi_1_2_0(); + // writeFileSync( + // `${process.env.STAINLESS_PATH.replace("\\ ", " ")}/openapi.yml`, + // yamlContent, + // "utf8", + // ); - console.log( - `OpenAPI document exported to ${process.env.STAINLESS_PATH}/openapi.yml`, - ); + // console.log( + // `OpenAPI document exported to ${process.env.STAINLESS_PATH}/openapi.yml`, + // ); // Run the run.sh script if it exists const runScriptPath = `${process.env.STAINLESS_PATH.replace("\\ ", " ")}/run.sh`; diff --git a/shared/api/products/productsOpenApi.ts b/shared/api/products/productsOpenApi.ts index 79937b4e3..895e4cf04 100644 --- a/shared/api/products/productsOpenApi.ts +++ b/shared/api/products/productsOpenApi.ts @@ -8,7 +8,7 @@ import { // Register schema with .meta() for OpenAPI spec generation export const ApiProductWithMeta = ApiProductSchema.meta({ - id: "Product", + // id: "Product", examples: [PRODUCT_EXAMPLE], }); @@ -17,11 +17,11 @@ export const productOps = { get: { summary: "List Products", tags: ["products"], - requestParams: { - query: z.object({ - customer_id: z.string().optional(), - }), - }, + // requestParams: { + // query: z.object({ + // customer_id: z.string().optional(), + // }), + // }, responses: { "200": { description: "",