From 101e1b05310f3968f85b9bbe87459b024c96909b Mon Sep 17 00:00:00 2001 From: Alexandre Stahmer Date: Wed, 31 Aug 2022 23:55:34 +0200 Subject: [PATCH] feat(openApiToTypescript): handle additionalProperties --- src/openApiToTypescript.test.ts | 32 ++++++++++++++++ src/openApiToTypescript.ts | 65 ++++++++++++++++++++------------- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/openApiToTypescript.test.ts b/src/openApiToTypescript.test.ts index 6312636b..8593c47d 100644 --- a/src/openApiToTypescript.test.ts +++ b/src/openApiToTypescript.test.ts @@ -115,6 +115,38 @@ test("getSchemaAsTsString", () => { }>;" `); + expect( + getSchemaAsTsString( + { type: "object", properties: { str: { type: "string" } }, additionalProperties: { type: "number" } }, + { name: "ObjectWithAdditionalPropsNb" } + ) + ).toMatchInlineSnapshot(` + "export type ObjectWithAdditionalPropsNb = Partial<{ + str: string; + } & { + [key: string]: number; + }>;" + `); + + expect( + getSchemaAsTsString( + { + type: "object", + properties: { str: { type: "string" } }, + additionalProperties: { type: "object", properties: { prop: { type: "boolean" } } }, + }, + { name: "ObjectWithNestedRecordBoolean" } + ) + ).toMatchInlineSnapshot(` + "export type ObjectWithNestedRecordBoolean = Partial<{ + str: string; + } & { + [key: string]: Partial<{ + prop: boolean; + }>; + }>;" + `); + expect( getSchemaAsTsString({ type: "array", diff --git a/src/openApiToTypescript.ts b/src/openApiToTypescript.ts index c87bfa62..84a4b56e 100644 --- a/src/openApiToTypescript.ts +++ b/src/openApiToTypescript.ts @@ -124,6 +124,41 @@ export const getTypescriptFromOpenApi = ({ canBeWrapped = false; const isPartial = !schema.required?.length; + let additionalProperties; + if (schema.additionalProperties) { + let additionalPropertiesType; + if ( + (typeof schema.additionalProperties === "boolean" && schema.additionalProperties) || + (typeof schema.additionalProperties === "object" && + Object.keys(schema.additionalProperties).length === 0) + ) { + additionalPropertiesType = t.any(); + } else if (typeof schema.additionalProperties === "object") { + additionalPropertiesType = getTypescriptFromOpenApi({ + schema: schema.additionalProperties, + ctx, + meta, + }); + } + + additionalProperties = ts.factory.createTypeLiteralNode([ + ts.factory.createIndexSignature( + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("key"), + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined + ), + ], + additionalPropertiesType as ts.TypeNode + ), + ]); + } + const props = Object.fromEntries( Object.entries(schema.properties!).map(([prop, propSchema]) => { let propType = getTypescriptFromOpenApi({ schema: propSchema, ctx, meta }) as TypeDefinition; @@ -138,32 +173,20 @@ export const getTypescriptFromOpenApi = ({ }) ); + const objectType = additionalProperties ? t.intersection([props, additionalProperties]) : props; + if (isInline) { - return isPartial ? t.reference("Partial", [props]) : props; + return isPartial ? t.reference("Partial", [objectType]) : objectType; } if (!inheritedMeta?.name) { throw new Error("Name is required to convert an object schema to a type reference"); } - // let additionalProps = ""; - // TODO - // if ( - // (typeof schema.additionalProperties === "boolean" && schema.additionalProperties) || - // (typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0) - // ) { - // additionalProps = ".passthrough()"; - // } else if (typeof schema.additionalProperties === "object") { - // // TODO maybe z.lazy - // return ( - // `z.record(${getTypescriptFromOpenApi(schema.additionalProperties)})` - // ); - // } - - const base = t.type(inheritedMeta.name, props); + const base = t.type(inheritedMeta.name, objectType); if (!isPartial) return base; - return t.type(inheritedMeta.name, t.reference("Partial", [props])); + return t.type(inheritedMeta.name, t.reference("Partial", [objectType])); } throw new Error(`Unsupported schema type: ${schema.type}`); @@ -199,11 +222,3 @@ const wrapTypeIfInline = ({ return typeDef as ts.Node; }; - -// https://cs.github.com/leancodepl/contractsgenerator-typescript/blob/c897eaab9dfa3bc0c08a67322759c94b3b0326b0/src/typesGeneration/types/GeneratorKnownType.ts?q=createIdentifier%28%22Partial%22%29#L14 -// t.reference(ts.factory.createIdentifier("Partial"), [ -// t.reference(ts.factory.createIdentifier("Record"), [ -// ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), -// ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), -// ]), -// ])