-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(codegen): x-expanded-relations (#3442)
## What Alter generated types base on `x-expanded-relations` OAS extension declared on schemaObjects. ## Why Often, API endpoints will automatically expand a model relations by default. They can also decorate a model with calculated totals. In order to more accurately represent the API, we wish to alter the generated types based on the expanded relations information. ## How - Follow the relation declaration signature as the backend controllers and the `expand` query param, i.e.: `items.variant.product`. - Introduce a custom `x-expended-relations` OAS extension. - Allow for organizing declared relations to help their maintenance. - Use traversal algorithms in codegen to support deeply nested relationships. - Use [type-fest](https://www.npmjs.com/package/type-fest)'s `Merge` and `SetRequired` to efficiently alter the types while enabling great intellisense for IDEs. Extra scope: * Added convenience yarn script to interact with the `medisa-oas` CLI within the monorepo. ## Test Include in the PR are two implementations of the x-expanded-relations on OAS schema, a simple and a complex one. ### Step 1 * Run `yarn install` * Run `yarn build` * Run `yarn medusa-oas oas --type combined --out-dir ~/tmp/oas` * Run `yarn medusa-oas client --type combined --component types --src-file ~/tmp/oas/combined.osa.json --out-dir ~/tmp/types` * Open `~/tmp/types/models/StoreRegionsRes` * Expect relations to be declared as required ### Step 2 * Open `~/tmp/types/models/StoreCartsRes` * Expect relations to be declared as required * Expect nested relations to have relations as required. ### Step 3 (optional) * Open `~/tmp/types` in an intellisense capable IDE * Within the `index.ts` file, attempt to declare a `const storeRegionRes: StoreRegionRes = {}` * Expect IDE to highlight that `countries` is a required field of `StoreRegionRes`
- Loading branch information
1 parent
c16f387
commit 7b57695
Showing
17 changed files
with
349 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@medusajs/openapi-typescript-codegen": minor | ||
--- | ||
|
||
feat(codegen): x-expanded-relations |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
...ypescript-codegen/src/openApi/v3/interfaces/Extensions/WithDefaultRelationsExtension.d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export interface WithExtendedRelationsExtension { | ||
"x-expanded-relations"?: { | ||
field: string | ||
relations?: string[] | ||
totals?: string[] | ||
implicit?: string[] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
packages/oas/openapi-typescript-codegen/src/openApi/v3/parser/getModelsExpandedRelations.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} |
Oops, something went wrong.