diff --git a/.changeset/tame-forks-marry.md b/.changeset/tame-forks-marry.md new file mode 100644 index 0000000000000..44b05aaff2180 --- /dev/null +++ b/.changeset/tame-forks-marry.md @@ -0,0 +1,5 @@ +--- +"@medusajs/openapi-typescript-codegen": minor +--- + +feat(codegen): x-expanded-relations diff --git a/package.json b/package.json index 165b0b40cc880..bd0a13c44c3fc 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "test:integration:plugins": "turbo run test --no-daemon --filter=integration-tests-plugins", "test:integration:repositories": "turbo run test --no-daemon --filter=integration-tests-repositories", "openapi:generate": "yarn ./packages/oas/oas-github-ci run ci", + "medusa-oas": "yarn ./packages/oas/medusa-oas-cli run medusa-oas", "generate:services": "typedoc --options typedoc.services.js", "generate:js-client": "typedoc --options typedoc.js-client.js", "generate:entities": "typedoc --options typedoc.entities.js", diff --git a/packages/medusa/src/api/routes/store/carts/index.ts b/packages/medusa/src/api/routes/store/carts/index.ts index 228bd222b5ae1..e1d534ca18b08 100644 --- a/packages/medusa/src/api/routes/store/carts/index.ts +++ b/packages/medusa/src/api/routes/store/carts/index.ts @@ -154,6 +154,52 @@ export const defaultStoreCartRelations = [ /** * @schema StoreCartsRes * type: object + * x-expanded-relations: + * field: cart + * relations: + * - billing_address + * - discounts + * - discounts.rule + * - gift_cards + * - items + * - items.adjustments + * - items.variant + * - payment + * - payment_sessions + * - region + * - region.countries + * - region.payment_providers + * - shipping_address + * - shipping_methods + * - shipping_methods.shipping_option + * implicit: + * - items.tax_lines + * - items.variant.product + * - region.fulfillment_providers + * - region.payment_providers + * - region.tax_rates + * - shipping_methods.shipping_option + * - shipping_methods.tax_lines + * totals: + * - discount_total + * - gift_card_tax_total + * - gift_card_total + * - item_tax_total + * - refundable_amount + * - refunded_total + * - shipping_tax_total + * - shipping_total + * - subtotal + * - tax_total + * - total + * - items.discount_total + * - items.gift_card_total + * - items.original_tax_total + * - items.original_total + * - items.refundable + * - items.subtotal + * - items.tax_total + * - items.total * required: * - cart * properties: diff --git a/packages/medusa/src/api/routes/store/regions/index.ts b/packages/medusa/src/api/routes/store/regions/index.ts index d3f7928106043..7f7d72ed2c209 100644 --- a/packages/medusa/src/api/routes/store/regions/index.ts +++ b/packages/medusa/src/api/routes/store/regions/index.ts @@ -16,6 +16,12 @@ export default (app) => { /** * @schema StoreRegionsListRes * type: object + * x-expanded-relations: + * field: regions + * relations: + * - countries + * - payment_providers + * - fulfillment_providers * required: * - regions * properties: @@ -31,6 +37,12 @@ export type StoreRegionsListRes = { /** * @schema StoreRegionsRes * type: object + * x-expanded-relations: + * field: region + * relations: + * - countries + * - payment_providers + * - fulfillment_providers * required: * - region * properties: diff --git a/packages/oas/medusa-oas-cli/package.json b/packages/oas/medusa-oas-cli/package.json index 9a02005d32e3f..608c04da0e225 100644 --- a/packages/oas/medusa-oas-cli/package.json +++ b/packages/oas/medusa-oas-cli/package.json @@ -31,7 +31,7 @@ "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", "build": "tsc --build", - "cli": "ts-node src/index.ts", + "medusa-oas": "ts-node src/index.ts", "test": "jest src", "test:unit": "jest src" }, diff --git a/packages/oas/medusa-oas-cli/src/__tests__/command-oas.test.ts b/packages/oas/medusa-oas-cli/src/__tests__/command-oas.test.ts index f9836230a8fdd..f7ff01e7d8bc0 100644 --- a/packages/oas/medusa-oas-cli/src/__tests__/command-oas.test.ts +++ b/packages/oas/medusa-oas-cli/src/__tests__/command-oas.test.ts @@ -21,7 +21,7 @@ const getTmpDirectory = async () => { } const runCLI = async (command: string, options: string[] = []) => { - const params = ["run", "cli", command, ...options] + const params = ["run", "medusa-oas", command, ...options] try { const { all: logs } = await execa("yarn", params, { cwd: basePath, diff --git a/packages/oas/openapi-typescript-codegen/src/client/interfaces/Model.d.ts b/packages/oas/openapi-typescript-codegen/src/client/interfaces/Model.d.ts index a3f46af4b2168..cb3f71c3e7d67 100644 --- a/packages/oas/openapi-typescript-codegen/src/client/interfaces/Model.d.ts +++ b/packages/oas/openapi-typescript-codegen/src/client/interfaces/Model.d.ts @@ -1,6 +1,14 @@ import type { Enum } from "./Enum" import type { Schema } from "./Schema" +export type NestedRelation = { + field: string + nestedRelations: NestedRelation[] + base?: string + isArray?: boolean + hasDepth?: boolean +} + export interface Model extends Schema { name: string export: @@ -24,4 +32,5 @@ export interface Model extends Schema { enum: Enum[] enums: Model[] properties: Model[] + nestedRelations?: NestedRelation[] } diff --git a/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/Extensions/WithDefaultRelationsExtension.d.ts b/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/Extensions/WithDefaultRelationsExtension.d.ts new file mode 100644 index 0000000000000..2a794852ea4b5 --- /dev/null +++ b/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/Extensions/WithDefaultRelationsExtension.d.ts @@ -0,0 +1,8 @@ +export interface WithExtendedRelationsExtension { + "x-expanded-relations"?: { + field: string + relations?: string[] + totals?: string[] + implicit?: string[] + } +} diff --git a/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/OpenApiSchema.d.ts b/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/OpenApiSchema.d.ts index 3d4f597087228..279bc9ad5bd51 100644 --- a/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/OpenApiSchema.d.ts +++ b/packages/oas/openapi-typescript-codegen/src/openApi/v3/interfaces/OpenApiSchema.d.ts @@ -4,11 +4,15 @@ import type { OpenApiDiscriminator } from "./OpenApiDiscriminator" import type { OpenApiExternalDocs } from "./OpenApiExternalDocs" import type { OpenApiReference } from "./OpenApiReference" import type { OpenApiXml } from "./OpenApiXml" +import { WithExtendedRelationsExtension } from "./Extensions/WithDefaultRelationsExtension" /** * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject */ -export interface OpenApiSchema extends OpenApiReference, WithEnumExtension { +export interface OpenApiSchema + extends OpenApiReference, + WithEnumExtension, + WithExtendedRelationsExtension { title?: string multipleOf?: number maximum?: number diff --git a/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModels.ts b/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModels.ts index 777da97586f0a..4c37dd8a1c644 100644 --- a/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModels.ts +++ b/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModels.ts @@ -8,6 +8,7 @@ import { OpenApiSchema } from "../interfaces/OpenApiSchema" import { Dictionary } from "../../../utils/types" import { OpenApiParameter } from "../interfaces/OpenApiParameter" import { listOperations } from "./listOperations" +import { handleExpandedRelations } from "./getModelsExpandedRelations" export const getModels = (openApi: OpenApi): Model[] => { const models: Model[] = [] @@ -27,6 +28,10 @@ export const getModels = (openApi: OpenApi): Model[] => { } } + for (const model of models) { + handleExpandedRelations(model, models) + } + /** * Bundle all query parameters in a single typed object * when x-codegen.queryParams is declared on the operation. diff --git a/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModelsExpandedRelations.ts b/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModelsExpandedRelations.ts new file mode 100644 index 0000000000000..e61d966c72058 --- /dev/null +++ b/packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModelsExpandedRelations.ts @@ -0,0 +1,128 @@ +import { Model, NestedRelation } from "../../../client/interfaces/Model" + +export const handleExpandedRelations = (model: Model, allModels: Model[]) => { + const xExpandedRelation = model.spec["x-expanded-relations"] + if (!xExpandedRelation) { + return + } + const field = xExpandedRelation.field + const relations = xExpandedRelation.relations ?? [] + const totals = xExpandedRelation.totals ?? [] + const implicit = xExpandedRelation.implicit ?? [] + + const nestedRelation: NestedRelation = { + field, + nestedRelations: [], + } + + for (const relation of [...relations, ...totals, ...implicit]) { + const splitRelation = relation.split(".") + walkSplitRelations(nestedRelation, splitRelation, 0) + } + + walkNestedRelations(allModels, model, model, nestedRelation) + model.imports = [...new Set(model.imports)] + + const prop = getPropertyByName(nestedRelation.field, model) + if (prop) { + prop.nestedRelations = [nestedRelation] + } +} + +const walkSplitRelations = ( + parentNestedRelation: NestedRelation, + splitRelation: string[], + depthIndex: number +) => { + const field = splitRelation[depthIndex] + let nestedRelation: NestedRelation | undefined = + parentNestedRelation.nestedRelations.find( + (nestedRelation) => nestedRelation.field === field + ) + if (!nestedRelation) { + nestedRelation = { + field, + nestedRelations: [], + } + parentNestedRelation.nestedRelations.push(nestedRelation) + } + depthIndex++ + if (depthIndex < splitRelation.length) { + walkSplitRelations(nestedRelation, splitRelation, depthIndex) + } +} + +const walkNestedRelations = ( + allModels: Model[], + rootModel: Model, + model: Model, + nestedRelation: NestedRelation, + parentNestedRelation?: NestedRelation +) => { + const prop = + model.export === "all-of" + ? findPropInAllOf(nestedRelation.field, model, allModels) + : getPropertyByName(nestedRelation.field, model) + if (!prop) { + return + } + if (!["reference", "array"].includes(prop.export)) { + return + } + + nestedRelation.base = prop.type + nestedRelation.isArray = prop.export === "array" + + for (const childNestedRelation of nestedRelation.nestedRelations) { + const childModel = getModelByName(prop.type, allModels) + if (!childModel) { + return + } + rootModel.imports.push(prop.type) + if (parentNestedRelation) { + parentNestedRelation.hasDepth = true + } + walkNestedRelations( + allModels, + rootModel, + childModel, + childNestedRelation, + nestedRelation + ) + } +} + +const findPropInAllOf = ( + fieldName: string, + model: Model, + allModels: Model[] +) => { + for (const property of model.properties) { + switch (property.export) { + case "interface": + return getPropertyByName(fieldName, model) + case "reference": + const tmpModel = getModelByName(property.type, allModels) + if (tmpModel) { + return getPropertyByName(fieldName, tmpModel) + } + break + } + } +} + +function getModelByName(name: string, models: Model[]): Model | void { + for (const model of models) { + if (model.name === name) { + return model + } + } +} + +function getPropertyByName(name: string, model: Model): Model | void { + for (const property of model.properties) { + if (property.name === name) { + return property + } + } +} diff --git a/packages/oas/openapi-typescript-codegen/src/templates/core/ModelUtils.hbs b/packages/oas/openapi-typescript-codegen/src/templates/core/ModelUtils.hbs new file mode 100644 index 0000000000000..cc754711e31ea --- /dev/null +++ b/packages/oas/openapi-typescript-codegen/src/templates/core/ModelUtils.hbs @@ -0,0 +1,94 @@ +{{>header}} + +/** + * Typing utilities from https://github.com/sindresorhus/type-fest + */ + +/** + * private methods for exportable dependencies +*/ +// https://github.com/sindresorhus/type-fest/blob/main/source/except.d.ts +type Filter = IsEqual extends true ? never : (KeyType extends ExcludeType ? never : KeyType); + +// https://github.com/sindresorhus/type-fest/blob/main/source/enforce-optional.d.ts +type RequiredFilter = undefined extends Type[Key] + ? Type[Key] extends undefined + ? Key + : never + : Key; + +type OptionalFilter = undefined extends Type[Key] + ? Type[Key] extends undefined + ? never + : Key + : never; + +// https://github.com/sindresorhus/type-fest/blob/main/source/merge.d.ts +type SimpleMerge = { + [Key in keyof Destination | keyof Source]: Key extends keyof Source + ? Source[Key] + : Key extends keyof Destination + ? Destination[Key] + : never; +}; + +/** + * optional exportable dependencies + */ +export type Simplify = {[KeyType in keyof T]: T[KeyType]} & {}; + +export type IsEqual = + (() => G extends A ? 1 : 2) extends + (() => G extends B ? 1 : 2) + ? true + : false; + +export type Except = { + [KeyType in keyof ObjectType as Filter]: ObjectType[KeyType]; +}; + +export type OmitIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record + ? never + : KeyType]: ObjectType[KeyType]; +}; + +export type PickIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record + ? KeyType + : never]: ObjectType[KeyType]; +}; + +export type EnforceOptional = Simplify<{ + [Key in keyof ObjectType as RequiredFilter]: ObjectType[Key] +} & { + [Key in keyof ObjectType as OptionalFilter]?: Exclude +}>; + +/** + * SetRequired + */ +export type SetRequired = + Simplify< + // Pick just the keys that are optional from the base type. + Except & + // Pick the keys that should be required from the base type and make them required. + Required> + >; + +/** + * SetNonNullable + */ +export type SetNonNullable = { + [Key in keyof BaseType]: Key extends Keys + ? NonNullable + : BaseType[Key]; +}; + +/** + * Merge + */ +export type Merge = EnforceOptional< + SimpleMerge, PickIndexSignature> + & SimpleMerge, OmitIndexSignature> +>; \ No newline at end of file diff --git a/packages/oas/openapi-typescript-codegen/src/templates/exportModel.hbs b/packages/oas/openapi-typescript-codegen/src/templates/exportModel.hbs index 7ddb2c3e8f1b7..aba18c7e2ec0f 100644 --- a/packages/oas/openapi-typescript-codegen/src/templates/exportModel.hbs +++ b/packages/oas/openapi-typescript-codegen/src/templates/exportModel.hbs @@ -1,5 +1,6 @@ {{>header}} +import { SetRequired, Merge } from '../core/ModelUtils'; {{#if imports}} {{#each imports}} diff --git a/packages/oas/openapi-typescript-codegen/src/templates/partials/exportInterface.hbs b/packages/oas/openapi-typescript-codegen/src/templates/partials/exportInterface.hbs index b61e26441bd7c..d62fbe251a057 100644 --- a/packages/oas/openapi-typescript-codegen/src/templates/partials/exportInterface.hbs +++ b/packages/oas/openapi-typescript-codegen/src/templates/partials/exportInterface.hbs @@ -8,7 +8,7 @@ {{/if}} */ {{/ifdef}} -export type {{{name}}} = { +export interface {{{name}}} { {{#each properties}} {{#ifdef description deprecated}} /** @@ -20,9 +20,16 @@ export type {{{name}}} = { {{/if}} */ {{/ifdef}} - {{>isReadOnly}}{{{name}}}{{>isRequired}}: {{>type parent=../name}}; + {{~#if nestedRelations}} + {{#each nestedRelations}} + {{>isReadOnly}}{{{../name}}}: {{>typeWithRelation}}; + {{/each}} + {{else}} + {{>isReadOnly}}{{{name}}}{{>isRequired}}: {{>type parent=../name}}; + {{/if}} {{/each}} }; + {{#if enums}} {{#unless @root.useUnionTypes}} diff --git a/packages/oas/openapi-typescript-codegen/src/templates/partials/typeWithRelation.hbs b/packages/oas/openapi-typescript-codegen/src/templates/partials/typeWithRelation.hbs new file mode 100644 index 0000000000000..46ceadef56d6b --- /dev/null +++ b/packages/oas/openapi-typescript-codegen/src/templates/partials/typeWithRelation.hbs @@ -0,0 +1,11 @@ +{{~#if isArray}}Array<{{/if~}} +{{~#if hasDepth}}Merge<{{/if~}} +SetRequired<{{>base}}, {{#each nestedRelations}}'{{{field}}}'{{#unless @last}} | {{/unless}}{{/each}}>{{#if hasDepth}}, { +{{/if}} +{{~#each nestedRelations~}} + {{#if nestedRelations~}} + {{{field}}}: {{>typeWithRelation}}, + {{/if~}} +{{~/each~}} +{{~#if hasDepth~}} }>{{/if~}} +{{~#if isArray}}>{{/if~}} \ No newline at end of file diff --git a/packages/oas/openapi-typescript-codegen/src/utils/registerHandlebarTemplates.ts b/packages/oas/openapi-typescript-codegen/src/utils/registerHandlebarTemplates.ts index ce9464ad5b844..d541e3124bf22 100644 --- a/packages/oas/openapi-typescript-codegen/src/utils/registerHandlebarTemplates.ts +++ b/packages/oas/openapi-typescript-codegen/src/utils/registerHandlebarTemplates.ts @@ -7,6 +7,7 @@ import templateCoreApiError from "../templates/core/ApiError.hbs" import templateCoreApiRequestOptions from "../templates/core/ApiRequestOptions.hbs" import templateCoreApiResult from "../templates/core/ApiResult.hbs" import templateCoreHookUtils from "../templates/core/HookUtils.hbs" +import templateCoreModelUtils from "../templates/core/ModelUtils.hbs" import axiosGetHeaders from "../templates/core/axios/getHeaders.hbs" import axiosGetRequestBody from "../templates/core/axios/getRequestBody.hbs" import axiosGetResponseBody from "../templates/core/axios/getResponseBody.hbs" @@ -84,6 +85,7 @@ import partialTypeInterface from "../templates/partials/typeInterface.hbs" import partialTypeIntersection from "../templates/partials/typeIntersection.hbs" import partialTypeReference from "../templates/partials/typeReference.hbs" import partialTypeUnion from "../templates/partials/typeUnion.hbs" +import partialTypeWithRelation from "../templates/partials/typeWithRelation.hbs" import { registerHandlebarHelpers } from "./registerHandlebarHelpers" export interface Templates { @@ -111,6 +113,7 @@ export interface Templates { baseHttpRequest: Handlebars.TemplateDelegate httpRequest: Handlebars.TemplateDelegate hookUtils: Handlebars.TemplateDelegate + modelUtils: Handlebars.TemplateDelegate } } @@ -151,6 +154,7 @@ export const registerHandlebarTemplates = (root: { baseHttpRequest: Handlebars.template(templateCoreBaseHttpRequest), httpRequest: Handlebars.template(templateCoreHttpRequest), hookUtils: Handlebars.template(templateCoreHookUtils), + modelUtils: Handlebars.template(templateCoreModelUtils), }, } @@ -242,6 +246,10 @@ export const registerHandlebarTemplates = (root: { "typeIntersection", Handlebars.template(partialTypeIntersection) ) + Handlebars.registerPartial( + "typeWithRelation", + Handlebars.template(partialTypeWithRelation) + ) Handlebars.registerPartial("base", Handlebars.template(partialBase)) // Generic functions used in 'request' file @see src/templates/core/request.hbs for more info diff --git a/packages/oas/openapi-typescript-codegen/src/utils/writeClient.ts b/packages/oas/openapi-typescript-codegen/src/utils/writeClient.ts index de610f261a2eb..0245de554499c 100644 --- a/packages/oas/openapi-typescript-codegen/src/utils/writeClient.ts +++ b/packages/oas/openapi-typescript-codegen/src/utils/writeClient.ts @@ -171,6 +171,11 @@ export const writeClient = async ( if (exportModels) { await rmdir(outputPathModels) await mkdir(outputPathModels) + await mkdir(outputPathCore) + await writeFile( + resolve(outputPathCore, "ModelUtils.ts"), + i(templates.core.modelUtils({}), indent) + ) await writeClientModels( client.models, templates,