From be29fede7bcccbcb2be4d030c2cf7d6f0b734afd Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Wed, 11 Feb 2026 21:44:37 -0500 Subject: [PATCH 01/15] Added typescript-express-zod-server and typescript-express-zod-client --- .../typescript-express-zod-petstore-new.yaml | 6 + .../TypescriptExpressZodClientCodegen.java | 765 +++++++++++++++ .../TypescriptExpressZodServerCodegen.java | 902 ++++++++++++++++++ .../org.openapitools.codegen.CodegenConfig | 3 + .../api.mustache | 1 + .../dtos/mappers.mustache | 122 +++ .../dtos/types.mustache | 119 +++ .../http-client.mustache | 218 +++++ .../index.mustache | 9 + .../model.mustache | 1 + .../schemas.mustache | 59 ++ .../types.mustache | 121 +++ .../api.mustache | 0 .../dtos/mappers.mustache | 122 +++ .../dtos/types.mustache | 119 +++ .../express/auth-middleware.mustache | 58 ++ .../express/errors.mustache | 71 ++ .../express/handlers.mustache | 152 +++ .../express/index.mustache | 5 + .../express/router-factory.mustache | 147 +++ .../express/types.mustache | 49 + .../model.mustache | 0 .../schemas.mustache | 75 ++ .../types.mustache | 121 +++ ...xpressZodServerCodegenOptionsProvider.java | 28 + ...criptExpressZodServerCodegenModelTest.java | 29 + ...iptExpressZodServerCodegenOptionsTest.java | 30 + ...TypescriptExpressZodServerCodegenTest.java | 19 + 28 files changed, 3351 insertions(+) create mode 100644 bin/configs/typescript-express-zod-petstore-new.yaml create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/auth-middleware.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/types.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/model.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java diff --git a/bin/configs/typescript-express-zod-petstore-new.yaml b/bin/configs/typescript-express-zod-petstore-new.yaml new file mode 100644 index 000000000000..59e3bb02a6b5 --- /dev/null +++ b/bin/configs/typescript-express-zod-petstore-new.yaml @@ -0,0 +1,6 @@ +generatorName: typescript-express-zod +outputDir: samples/server/petstore/typescript/express/zod +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-express-zod +additionalProperties: + hideGenerationTimestamp: "true" diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java new file mode 100644 index 000000000000..64308eb77499 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java @@ -0,0 +1,765 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.*; +import org.openapitools.codegen.model.ApiInfoMap; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public class TypescriptExpressZodClientCodegen extends AbstractTypeScriptClientCodegen { + private final Logger LOGGER = LoggerFactory.getLogger(TypescriptExpressZodClientCodegen.class); + + public TypescriptExpressZodClientCodegen() { + super(); + + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.EXPERIMENTAL) + .build(); + + outputFolder = "generated-code" + File.separator + "typescript-express-zod-client"; + embeddedTemplateDir = templateDir = "typescript-express-zod-client"; + + // We generate everything via supporting files, not per-model/per-api + modelTemplateFiles.put("model.mustache", ".ts"); + apiTemplateFiles.put("api.mustache", ".ts"); + + apiPackage = ""; + modelPackage = ""; + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "typescript-express-zod-client"; + } + + @Override + public String getHelp() { + return "Generates a TypeScript HTTP client with Zod response validation for an Express API."; + } + + @Override + public void processOpts() { + super.processOpts(); + + supportingFiles.clear(); + modelTemplateFiles.clear(); + apiTemplateFiles.clear(); + + supportingFiles.add(new SupportingFile("types.mustache", "", "types.ts")); + supportingFiles.add(new SupportingFile("schemas.mustache", "", "schemas.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "types.mustache", "dtos", "types.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "mappers.mustache", "dtos", "mappers.ts")); + supportingFiles.add(new SupportingFile("http-client.mustache", "", "http-client.ts")); + supportingFiles.add(new SupportingFile("index.mustache", "", "index.ts")); + } + + @Override + public OperationsMap postProcessOperationsWithModels(OperationsMap operations, List allModels) { + OperationMap objs = operations.getOperations(); + List ops = objs.getOperation(); + + for (CodegenOperation op : ops) { + // Build URL template for JS template literals: + // /recipe/{recipeId} -> /recipe/${encodeURIComponent(params.recipeId)} + String urlTemplate = op.path.replaceAll( + "\\{([^}]+)\\}", + "\\${encodeURIComponent(String(params.$1))}" + ); + op.vendorExtensions.put("x-url-template", urlTemplate); + + // Determine the service name from the tag + if (op.tags != null && !op.tags.isEmpty()) { + String tag = op.tags.get(0).getName(); + String serviceName = camelize(tag) + "Service"; + op.vendorExtensions.put("x-service-name", serviceName); + } + + // Set fetch method only for non-GET (GET is fetch default) + String httpMethodUpper = op.httpMethod.toUpperCase(Locale.ENGLISH); + if (!"GET".equals(httpMethodUpper)) { + op.vendorExtensions.put("x-fetch-method", httpMethodUpper); + } + + // Determine the response type and schema names + if (op.returnType != null) { + op.vendorExtensions.put("x-response-type", op.returnType); + op.vendorExtensions.put("x-response-schema", op.returnType + "Schema"); + } + + // Build params type name + String paramsType = camelize(op.operationId) + "Params"; + op.vendorExtensions.put("x-params-type", paramsType); + + // Check if all params are optional + boolean allParamsOptional = true; + for (CodegenParameter param : op.allParams) { + if (param.required) { + allParamsOptional = false; + break; + } + } + op.vendorExtensions.put("x-all-params-optional", allParamsOptional); + + // Body param info + boolean hasBodyParam = op.bodyParam != null; + op.vendorExtensions.put("x-has-body-param", hasBodyParam); + if (hasBodyParam) { + String bodyParamName = op.bodyParam.paramName; + if (bodyParamName == null || bodyParamName.isEmpty()) { + bodyParamName = "body"; + } + op.vendorExtensions.put("x-body-param-name", bodyParamName); + } + + boolean hasNoParams = op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; + op.vendorExtensions.put("x-has-no-params", hasNoParams); + + // Add vendor extensions to each parameter for Zod schema building + for (CodegenParameter param : op.allParams) { + addZodVendorExtensions(param); + } + } + + return operations; + } + + /** + * Add Zod-specific vendor extensions to a parameter. + */ + private void addZodVendorExtensions(CodegenParameter param) { + String zodType = buildZodType(param); + param.vendorExtensions.put("x-zod-type", zodType); + + // Track if the query param name differs from the camelCase name + if (param.isQueryParam && !param.baseName.equals(param.paramName)) { + param.vendorExtensions.put("x-original-name", param.baseName); + param.vendorExtensions.put("x-has-different-name", true); + } + } + + /** + * Build a Zod type expression for a parameter. + */ + private String buildZodType(CodegenParameter param) { + StringBuilder zod = new StringBuilder(); + + if (param.isEnum) { + zod.append(camelize(param.dataType) + "Schema"); + } else if (!param.isPrimitiveType && param.dataType != null && !param.isArray + && !param.isBodyParam && !param.isModel) { + zod.append(camelize(param.dataType) + "Schema"); + } else if (param.isInteger || param.isLong) { + zod.append("z.coerce.number().int()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isNumber || param.isFloat || param.isDouble) { + zod.append("z.number()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isBoolean) { + zod.append("z.boolean()"); + } else if (param.isString) { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + if (param.minLength != null) { + zod.append(".min(").append(param.minLength).append(")"); + } + } else if (param.isArray) { + zod.append("z.array(z.string())"); // simplified + } else if (param.isModel || param.isBodyParam) { + zod.append(camelize(param.dataType) + "Schema"); + } else { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + } + + if (!param.required) { + zod.append(".optional()"); + } + + return zod.toString(); + } + + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + + // Collect enum model names + Set enumModelNames = new HashSet<>(); + for (ModelsMap modEntry : result.values()) { + for (ModelMap innerMo : modEntry.getModels()) { + CodegenModel innerCm = innerMo.getModel(); + if (innerCm.isEnum) { + enumModelNames.add(innerCm.classname); + } + } + } + + for (ModelsMap entry : result.values()) { + for (ModelMap mo : entry.getModels()) { + CodegenModel cm = mo.getModel(); + + // Add Zod vendor extensions to each property + for (CodegenProperty prop : cm.vars) { + addZodVendorExtensions(prop, cm); + } + + // Look up original schema from OAS spec (shared by oneOf and allOf detection) + Map allSchemas = ModelUtils.getSchemas(this.openAPI); + Schema origSchema = null; + for (Map.Entry schemaEntry : allSchemas.entrySet()) { + if (toModelName(schemaEntry.getKey()).equals(cm.classname)) { + origSchema = schemaEntry.getValue(); + break; + } + } + + // Determine if this is a discriminated union + if (cm.discriminator != null && cm.oneOf != null && !cm.oneOf.isEmpty()) { + cm.vendorExtensions.put("x-is-discriminated-union", true); + cm.vendorExtensions.put("x-discriminator-property", cm.discriminator.getPropertyBaseName()); + + List> mappedTypes = new ArrayList<>(); + + if (cm.discriminator.getMappedModels() != null && !cm.discriminator.getMappedModels().isEmpty()) { + for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) { + Map mapped = new HashMap<>(); + mapped.put("modelName", mm.getModelName()); + mapped.put("mappingName", mm.getMappingName()); + mappedTypes.add(mapped); + } + } else { + for (String memberName : cm.oneOf) { + Map mapped = new HashMap<>(); + mapped.put("modelName", memberName); + String discValue = findDiscriminatorValue(memberName, cm.discriminator.getPropertyBaseName(), result); + mapped.put("mappingName", discValue != null ? discValue : memberName); + mappedTypes.add(mapped); + } + } + cm.vendorExtensions.put("x-mapped-models", mappedTypes); + } + + // Detect non-discriminated oneOf unions from the original OAS spec + if (cm.discriminator == null && !cm.vendorExtensions.containsKey("x-is-discriminated-union")) { + if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { + cm.vendorExtensions.put("x-is-union", true); + + // Collect member classnames + List memberNames = new ArrayList<>(); + for (Object memberObj : origSchema.getOneOf()) { + Schema memberSchema = (Schema) memberObj; + String ref = memberSchema.get$ref(); + if (ref != null) { + memberNames.add(toModelName(ModelUtils.getSimpleRef(ref))); + } + } + + // Collect property sets per member to find distinguishing properties + Map> memberPropSets = new LinkedHashMap<>(); + for (String memberName : memberNames) { + Set props = new LinkedHashSet<>(); + ModelsMap memberModels = result.get(memberName); + if (memberModels != null) { + for (ModelMap mm : memberModels.getModels()) { + for (CodegenProperty prop : mm.getModel().vars) { + props.add(prop.name); + } + } + } + memberPropSets.put(memberName, props); + } + + // Build x-union-members list with distinguishing properties + List> members = new ArrayList<>(); + for (int i = 0; i < memberNames.size(); i++) { + String memberName = memberNames.get(i); + Map info = new HashMap<>(); + info.put("modelName", memberName); + + if (i < memberNames.size() - 1) { + // Find a property unique to this member + Set myProps = memberPropSets.getOrDefault(memberName, Collections.emptySet()); + for (String prop : myProps) { + boolean unique = true; + for (Map.Entry> other : memberPropSets.entrySet()) { + if (!other.getKey().equals(memberName) && other.getValue().contains(prop)) { + unique = false; + break; + } + } + if (unique) { + info.put("hasDistinguishingProperty", true); + info.put("distinguishingProperty", prop); + break; + } + } + } + // Last member is always the fallback (no distinguishing property needed) + members.add(info); + } + + cm.vendorExtensions.put("x-union-members", members); + } + } + + // Detect allOf intersection types + if (origSchema != null && origSchema.getAllOf() != null && !origSchema.getAllOf().isEmpty()) { + List parentNames = new ArrayList<>(); + for (Object member : origSchema.getAllOf()) { + Schema memberSchema = (Schema) member; + if (memberSchema.get$ref() != null) { + parentNames.add(toModelName(ModelUtils.getSimpleRef(memberSchema.get$ref()))); + } + } + + if (!parentNames.isEmpty()) { + cm.vendorExtensions.put("x-is-intersection", true); + + List> parents = new ArrayList<>(); + Set parentPropertyNames = new HashSet<>(); + for (String parentName : parentNames) { + Map parentInfo = new HashMap<>(); + parentInfo.put("modelName", parentName); + parents.add(parentInfo); + + ModelsMap parentModels = result.get(parentName); + if (parentModels != null) { + for (ModelMap pmm : parentModels.getModels()) { + for (CodegenProperty prop : pmm.getModel().vars) { + parentPropertyNames.add(prop.name); + } + } + } + } + cm.vendorExtensions.put("x-intersection-parents", parents); + + boolean hasOwnProperties = false; + for (CodegenProperty prop : cm.vars) { + if (!parentPropertyNames.contains(prop.name)) { + prop.vendorExtensions.put("x-is-own-property", true); + hasOwnProperties = true; + } + } + cm.vendorExtensions.put("x-has-own-properties", hasOwnProperties); + } + } + + // Check if model has properties that reference other object models or enums + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null && !prop.isArray) { + if (enumModelNames.contains(prop.complexType)) { + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.complexType); + } else { + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.complexType); + } + } else if (prop.isArray && prop.items != null && prop.items.complexType != null) { + if (enumModelNames.contains(prop.items.complexType)) { + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.items.complexType); + } else { + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.items.complexType); + } + } + } + + // Schema and DTO names + cm.vendorExtensions.put("x-schema-name", cm.classname + "Schema"); + cm.vendorExtensions.put("x-dto-name", cm.classname + "Dto"); + + // Check if has single-value enum properties (literal types) + for (CodegenProperty prop : cm.vars) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + prop.vendorExtensions.put("x-is-literal", true); + prop.vendorExtensions.put("x-literal-value", values.get(0).toString()); + } + } + } + } + } + + return result; + } + + /** + * Find the discriminator property's literal value in a member model. + */ + private String findDiscriminatorValue(String modelName, String discriminatorProperty, + Map allModels) { + ModelsMap modelsMap = allModels.get(modelName); + if (modelsMap == null) return null; + + for (ModelMap mo : modelsMap.getModels()) { + CodegenModel cm = mo.getModel(); + for (CodegenProperty prop : cm.vars) { + if (prop.baseName.equals(discriminatorProperty) || prop.name.equals(discriminatorProperty)) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + return values.get(0).toString(); + } + } + } + } + } + return null; + } + + /** + * Add Zod-specific vendor extensions to a model property. + */ + private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { + String zodType = buildZodPropertyType(prop); + prop.vendorExtensions.put("x-zod-type", zodType); + + String fullZod = zodType; + if (!prop.required) { + fullZod = zodType + ".optional()"; + } + prop.vendorExtensions.put("x-zod-full", fullZod); + } + + /** + * Build a Zod type expression for a model property. + */ + private String buildZodPropertyType(CodegenProperty prop) { + // Handle enum properties with allowable values + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + return "z.literal(\"" + values.get(0) + "\")"; + } + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + if (values != null) { + StringBuilder sb = new StringBuilder("z.enum(["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("\"").append(values.get(i)).append("\""); + } + sb.append("])"); + return sb.toString(); + } + } + + // Handle arrays (before complexType check) + if (prop.isArray && prop.items != null) { + String itemZod = buildZodPropertyType(prop.items); + return itemZod + ".array()"; + } + + // Handle $ref to another model or enum type + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + + // Handle primitive types + if (prop.isInteger || prop.isLong) { + StringBuilder sb = new StringBuilder("z.coerce.number().int()"); + if (prop.minimum != null) { + sb.append(".gte(").append(prop.minimum).append(")"); + } + if (prop.maximum != null) { + sb.append(".lte(").append(prop.maximum).append(")"); + } + return sb.toString(); + } + + if (prop.isNumber || prop.isFloat || prop.isDouble) { + StringBuilder sb = new StringBuilder("z.number()"); + if (prop.minimum != null) { + sb.append(".gte(").append(prop.minimum).append(")"); + } + if (prop.maximum != null) { + sb.append(".lte(").append(prop.maximum).append(")"); + } + return sb.toString(); + } + + if (prop.isBoolean) { + return "z.boolean()"; + } + + if (prop.isString) { + StringBuilder sb = new StringBuilder("z.string()"); + if (prop.maxLength != null) { + sb.append(".max(").append(prop.maxLength).append(")"); + } + if (prop.minLength != null) { + sb.append(".min(").append(prop.minLength).append(")"); + } + return sb.toString(); + } + + return "z.any()"; + } + + @Override + @SuppressWarnings("unchecked") + public Map postProcessSupportingFileData(Map bundle) { + bundle = super.postProcessSupportingFileData(bundle); + + List allOps = ((org.openapitools.codegen.model.ApiInfoMap) bundle.get("apiInfo")).getApis(); + + // Collect all unique services + Map>> serviceMap = new LinkedHashMap<>(); + + for (OperationsMap opsMap : allOps) { + OperationMap operationMap = opsMap.getOperations(); + List ops = operationMap.getOperation(); + + for (CodegenOperation op : ops) { + String serviceName = (String) op.vendorExtensions.getOrDefault("x-service-name", "DefaultService"); + + serviceMap.computeIfAbsent(serviceName, k -> new ArrayList<>()); + + Map opInfo = new HashMap<>(); + String operationIdPascal = camelize(op.operationId); + opInfo.put("operationId", op.operationId); + opInfo.put("operationIdPascal", operationIdPascal); + opInfo.put("description", op.summary != null ? op.summary : op.notes); + opInfo.put("httpMethod", op.httpMethod.toLowerCase(Locale.ENGLISH)); + opInfo.put("httpMethodUpper", op.httpMethod.toUpperCase(Locale.ENGLISH)); + opInfo.put("serviceName", serviceName); + opInfo.put("returnType", op.returnType); + opInfo.put("hasParams", !op.allParams.isEmpty()); + opInfo.put("hasNoParams", op.allParams.isEmpty()); + opInfo.put("allParamsOptional", op.vendorExtensions.get("x-all-params-optional")); + + // URL template for JS template literals + opInfo.put("urlTemplate", op.vendorExtensions.get("x-url-template")); + + // Fetch method (only for non-GET) + opInfo.put("fetchMethod", op.vendorExtensions.get("x-fetch-method")); + opInfo.put("hasFetchMethod", op.vendorExtensions.containsKey("x-fetch-method")); + + // Params info + List> pathParams = new ArrayList<>(); + for (CodegenParameter p : op.pathParams) { + Map pm = new HashMap<>(); + pm.put("name", p.paramName); + pm.put("baseName", p.baseName); + pm.put("zodType", buildZodType(p)); + pathParams.add(pm); + } + opInfo.put("pathParams", pathParams); + opInfo.put("hasPathParams", !pathParams.isEmpty()); + + List> queryParams = new ArrayList<>(); + for (CodegenParameter p : op.queryParams) { + Map qm = new HashMap<>(); + qm.put("name", p.paramName); + qm.put("baseName", p.baseName); + qm.put("hasDifferentName", !p.baseName.equals(p.paramName)); + qm.put("zodType", buildZodType(p)); + qm.put("dataType", p.dataType); + qm.put("required", p.required); + qm.put("isEnum", p.isEnum); + queryParams.add(qm); + } + opInfo.put("queryParams", queryParams); + opInfo.put("hasQueryParams", !queryParams.isEmpty()); + + if (op.bodyParam != null) { + Map body = new HashMap<>(); + body.put("name", op.vendorExtensions.get("x-body-param-name")); + body.put("dataType", op.bodyParam.dataType); + opInfo.put("bodyParam", body); + opInfo.put("hasBodyParam", true); + } else { + opInfo.put("hasBodyParam", false); + } + + // Response info + opInfo.put("responseType", op.returnType); + opInfo.put("responseSchema", op.vendorExtensions.get("x-response-schema")); + if (op.returnType != null) { + opInfo.put("responseDtoMapper", "mapFrom" + op.returnType + "Dto"); + opInfo.put("responseDtoType", op.returnType + "Dto"); + } + + // SSE detection + boolean hasSSE = false; + for (CodegenResponse response : op.responses) { + if (response.is2xx && response.getContent() != null) { + CodegenMediaType sseMt = response.getContent().get("text/event-stream"); + if (sseMt != null && sseMt.getSchema() != null) { + hasSSE = true; + String sseType = sseMt.getSchema().getComplexType(); + if (sseType == null) sseType = sseMt.getSchema().getDataType(); + opInfo.put("sseType", sseType); + opInfo.put("sseDtoType", sseType + "Dto"); + opInfo.put("sseDtoMapper", "mapFrom" + sseType + "Dto"); + opInfo.put("sseSchema", sseType + "Schema"); + opInfo.put("streamOperationId", op.operationId + "Stream"); + break; + } + } + } + opInfo.put("hasSSE", hasSSE); + + // Body DTO mapper + if (op.bodyParam != null) { + opInfo.put("bodyDtoMapper", "mapTo" + op.bodyParam.dataType + "Dto"); + } + + serviceMap.get(serviceName).add(opInfo); + } + } + + // Convert service map to list for Mustache iteration + List> services = new ArrayList<>(); + for (Map.Entry>> entry : serviceMap.entrySet()) { + Map svc = new HashMap<>(); + svc.put("name", entry.getKey()); + svc.put("operations", entry.getValue()); + boolean serviceHasSSE = entry.getValue().stream() + .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); + svc.put("hasSSE", serviceHasSSE); + services.add(svc); + } + bundle.put("services", services); + + // Collect all operations flat + List> allOperations = new ArrayList<>(); + for (Map svc : services) { + @SuppressWarnings("unchecked") + List> ops = (List>) svc.get("operations"); + allOperations.addAll(ops); + } + allOperations.sort(Comparator.comparing(o -> (String) o.get("operationId"))); + bundle.put("allOperations", allOperations); + + boolean anySSE = allOperations.stream() + .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); + bundle.put("hasAnySSE", anySSE); + + // Topologically sort models + @SuppressWarnings("unchecked") + List models = (List) bundle.get("models"); + if (models != null) { + bundle.put("models", topologicallySortModels(models)); + } + + return bundle; + } + + /** + * Topologically sort models so that leaf types come before models that reference them. + */ + @SuppressWarnings("unchecked") + private List topologicallySortModels(List models) { + Map modelByName = new LinkedHashMap<>(); + Map> dependencies = new LinkedHashMap<>(); + + for (Object modelEntry : models) { + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm == null) continue; + + modelByName.put(cm.classname, modelEntry); + + Set deps = new LinkedHashSet<>(); + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null) { + deps.add(prop.complexType); + } + if (prop.isArray && prop.items != null && prop.items.complexType != null) { + deps.add(prop.items.complexType); + } + } + if (cm.oneOf != null) { + deps.addAll(cm.oneOf); + } + // Also include non-discriminated union member dependencies + if (cm.vendorExtensions.containsKey("x-union-members")) { + @SuppressWarnings("unchecked") + List> unionMembers = + (List>) cm.vendorExtensions.get("x-union-members"); + for (Map member : unionMembers) { + deps.add((String) member.get("modelName")); + } + } + // Also include allOf intersection parent dependencies + if (cm.vendorExtensions.containsKey("x-intersection-parents")) { + @SuppressWarnings("unchecked") + List> intersectionParents = + (List>) cm.vendorExtensions.get("x-intersection-parents"); + for (Map parent : intersectionParents) { + deps.add(parent.get("modelName")); + } + } + deps.remove(cm.classname); + dependencies.put(cm.classname, deps); + } + + List sorted = new ArrayList<>(); + Set visited = new LinkedHashSet<>(); + Set visiting = new LinkedHashSet<>(); + + for (String name : modelByName.keySet()) { + if (!visited.contains(name)) { + topoVisit(name, modelByName, dependencies, visited, visiting, sorted); + } + } + + return sorted; + } + + private void topoVisit(String name, Map modelByName, + Map> dependencies, + Set visited, Set visiting, List sorted) { + if (visited.contains(name)) return; + if (visiting.contains(name)) return; // cycle detected, break it + visiting.add(name); + + Set deps = dependencies.getOrDefault(name, Collections.emptySet()); + for (String dep : deps) { + if (modelByName.containsKey(dep)) { + topoVisit(dep, modelByName, dependencies, visited, visiting, sorted); + } + } + + visiting.remove(name); + visited.add(name); + if (modelByName.containsKey(name)) { + sorted.add(modelByName.get(name)); + } + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java new file mode 100644 index 000000000000..1822ea08a3c2 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -0,0 +1,902 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.*; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public class TypescriptExpressZodServerCodegen extends AbstractTypeScriptClientCodegen { + private final Logger LOGGER = LoggerFactory.getLogger(TypescriptExpressZodServerCodegen.class); + + public TypescriptExpressZodServerCodegen() { + super(); + + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.EXPERIMENTAL) + .build(); + + outputFolder = "generated-code" + File.separator + "typescript-express-zod-server"; + embeddedTemplateDir = templateDir = "typescript-express-zod-server"; + + // We generate everything via supporting files, not per-model/per-api + modelTemplateFiles.put("model.mustache", ".ts"); + apiTemplateFiles.put("api.mustache", ".ts"); + + apiPackage = ""; + modelPackage = ""; + } + + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + + @Override + public String getName() { + return "typescript-express-zod-server"; + } + + @Override + public String getHelp() { + return "Generates a TypeScript Express server with Zod validation."; + } + + @Override + public void processOpts() { + super.processOpts(); + + supportingFiles.clear(); + modelTemplateFiles.clear(); + apiTemplateFiles.clear(); + + supportingFiles.add(new SupportingFile("types.mustache", "", "types.ts")); + supportingFiles.add(new SupportingFile("schemas.mustache", "", "schemas.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "types.mustache", "dtos", "types.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "mappers.mustache", "dtos", "mappers.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "handlers.mustache", "express", "handlers.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "types.mustache", "express", "types.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "router-factory.mustache", "express", "router-factory.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "errors.mustache", "express", "errors.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "auth-middleware.mustache", "express", "auth-middleware.ts")); + supportingFiles.add(new SupportingFile("express" + File.separator + "index.mustache", "express", "index.ts")); + } + + @Override + public OperationsMap postProcessOperationsWithModels(OperationsMap operations, List allModels) { + OperationMap objs = operations.getOperations(); + List ops = objs.getOperation(); + + for (CodegenOperation op : ops) { + // Convert {param} to :param for Express routing + op.path = op.path.replaceAll("\\{([^}]+)\\}", ":$1"); + + // Add vendor extensions for template use + op.vendorExtensions.put("x-express-path", op.path); + op.vendorExtensions.put("x-http-method-lower", op.httpMethod.toLowerCase(Locale.ENGLISH)); + + // Build the handler name: handle + PascalCase(operationId) + String handlerName = "handle" + camelize(op.operationId); + op.vendorExtensions.put("x-handler-name", handlerName); + + // Determine the service name from the tag + if (op.tags != null && !op.tags.isEmpty()) { + String tag = op.tags.get(0).getName(); + String serviceName = camelize(tag) + "Service"; + op.vendorExtensions.put("x-service-name", serviceName); + op.vendorExtensions.put("x-service-getter", "get" + serviceName); + } + + // Determine the response type and schema names + if (op.returnType != null) { + op.vendorExtensions.put("x-response-type", op.returnType); + op.vendorExtensions.put("x-response-schema", op.returnType + "Schema"); + op.vendorExtensions.put("x-response-dto-mapper", "mapTo" + op.returnType + "Dto"); + } + + // Mark whether this operation has only path params, only query params, etc. + boolean hasOnlyPathParams = !op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; + boolean hasOnlyQueryParams = op.pathParams.isEmpty() && !op.queryParams.isEmpty() && op.bodyParam == null; + boolean hasBodyParam = op.bodyParam != null; + boolean hasNoParams = op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; + + op.vendorExtensions.put("x-has-no-params", hasNoParams); + op.vendorExtensions.put("x-has-body-param", hasBodyParam); + + // Build params schema name + String paramsType = camelize(op.operationId) + "Params"; + op.vendorExtensions.put("x-params-type", paramsType); + op.vendorExtensions.put("x-params-schema", paramsType + "Schema"); + + // Check if all params are optional (for optional parse) + boolean allParamsOptional = true; + for (CodegenParameter param : op.allParams) { + if (param.required) { + allParamsOptional = false; + break; + } + } + op.vendorExtensions.put("x-all-params-optional", allParamsOptional); + + // For body params, determine the DTO mapper name + if (op.bodyParam != null) { + String bodyType = op.bodyParam.dataType; + String bodyMapperName = "mapFrom" + bodyType + "Dto"; + op.vendorExtensions.put("x-body-mapper", bodyMapperName); + op.vendorExtensions.put("x-body-type", bodyType); + + // Get the request body name from x-codegen-request-body-name or default to "body" + String bodyParamName = op.bodyParam.paramName; + if (bodyParamName == null || bodyParamName.isEmpty()) { + bodyParamName = "body"; + } + op.vendorExtensions.put("x-body-param-name", bodyParamName); + } + + // Add vendor extensions to each parameter for Zod schema building + for (CodegenParameter param : op.allParams) { + addZodVendorExtensions(param); + } + + // Build RequestHandler type generics + buildRequestHandlerType(op); + } + + return operations; + } + + /** + * Add Zod-specific vendor extensions to a parameter. + */ + private void addZodVendorExtensions(CodegenParameter param) { + String zodType = buildZodType(param); + param.vendorExtensions.put("x-zod-type", zodType); + + // Track if the query param name differs from the camelCase name + if (param.isQueryParam && !param.baseName.equals(param.paramName)) { + param.vendorExtensions.put("x-original-name", param.baseName); + param.vendorExtensions.put("x-has-different-name", true); + } + } + + /** + * Build a Zod type expression for a parameter. + */ + private String buildZodType(CodegenParameter param) { + StringBuilder zod = new StringBuilder(); + + if (param.isEnum) { + // Reference the enum schema + zod.append(camelize(param.dataType) + "Schema"); + } else if (!param.isPrimitiveType && param.dataType != null && !param.isArray + && !param.isBodyParam && !param.isModel) { + // Reference to a non-primitive type (e.g., enum via $ref) + zod.append(camelize(param.dataType) + "Schema"); + } else if (param.isInteger || param.isLong) { + zod.append("z.coerce.number().int()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isNumber || param.isFloat || param.isDouble) { + zod.append("z.number()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isBoolean) { + zod.append("z.boolean()"); + } else if (param.isString) { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + if (param.minLength != null) { + zod.append(".min(").append(param.minLength).append(")"); + } + } else if (param.isArray) { + zod.append("z.array(z.string())"); // simplified + } else if (param.isModel || param.isBodyParam) { + // Reference the model schema + zod.append(camelize(param.dataType) + "Schema"); + } else { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + } + + if (!param.required) { + zod.append(".optional()"); + } + + return zod.toString(); + } + + /** + * Build the Express RequestHandler type generics for an operation. + */ + private void buildRequestHandlerType(CodegenOperation op) { + // Params type (path params) + StringBuilder paramsType = new StringBuilder("{"); + if (!op.pathParams.isEmpty()) { + paramsType = new StringBuilder("{\n"); + for (int i = 0; i < op.pathParams.size(); i++) { + CodegenParameter p = op.pathParams.get(i); + paramsType.append(" ").append(p.paramName).append(": string"); + if (i < op.pathParams.size() - 1) paramsType.append(","); + paramsType.append("\n"); + } + paramsType.append(" }"); + } else { + paramsType.append("}"); + } + op.vendorExtensions.put("x-params-generic", paramsType.toString()); + + // Query type + StringBuilder queryType = new StringBuilder("{"); + if (!op.queryParams.isEmpty()) { + queryType = new StringBuilder("{\n"); + for (int i = 0; i < op.queryParams.size(); i++) { + CodegenParameter p = op.queryParams.get(i); + String qName = p.baseName.contains("-") ? "\"" + p.baseName + "\"" : p.baseName; + queryType.append(" ").append(qName).append("?: string"); + if (i < op.queryParams.size() - 1) queryType.append(","); + queryType.append("\n"); + } + queryType.append(" }"); + } else { + queryType.append("}"); + } + op.vendorExtensions.put("x-query-generic", queryType.toString()); + + // Body type + if (op.bodyParam != null) { + op.vendorExtensions.put("x-body-generic", "dtos." + op.bodyParam.dataType + "Dto"); + } else { + op.vendorExtensions.put("x-body-generic", "never"); + } + + // Response type + if (op.returnType != null) { + op.vendorExtensions.put("x-response-generic", "dtos." + op.returnType + "Dto"); + } else { + op.vendorExtensions.put("x-response-generic", "any"); + } + } + + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + + // Collect enum model names (enums don't need DTO mappers) + Set enumModelNames = new HashSet<>(); + for (ModelsMap modEntry : result.values()) { + for (ModelMap innerMo : modEntry.getModels()) { + CodegenModel innerCm = innerMo.getModel(); + if (innerCm.isEnum) { + enumModelNames.add(innerCm.classname); + } + } + } + + for (ModelsMap entry : result.values()) { + for (ModelMap mo : entry.getModels()) { + CodegenModel cm = mo.getModel(); + + // Add Zod vendor extensions to each property + for (CodegenProperty prop : cm.vars) { + addZodVendorExtensions(prop, cm); + } + + // Look up original schema from OAS spec (shared by oneOf and allOf detection) + Map allSchemas = ModelUtils.getSchemas(this.openAPI); + Schema origSchema = null; + for (Map.Entry schemaEntry : allSchemas.entrySet()) { + if (toModelName(schemaEntry.getKey()).equals(cm.classname)) { + origSchema = schemaEntry.getValue(); + break; + } + } + + // Determine if this is a discriminated union + if (cm.discriminator != null && cm.oneOf != null && !cm.oneOf.isEmpty()) { + cm.vendorExtensions.put("x-is-discriminated-union", true); + cm.vendorExtensions.put("x-discriminator-property", cm.discriminator.getPropertyBaseName()); + + List> mappedTypes = new ArrayList<>(); + + if (cm.discriminator.getMappedModels() != null && !cm.discriminator.getMappedModels().isEmpty()) { + // Use explicit mappings from the discriminator + for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) { + Map mapped = new HashMap<>(); + mapped.put("modelName", mm.getModelName()); + mapped.put("mappingName", mm.getMappingName()); + mappedTypes.add(mapped); + } + } else { + // Build mappings from oneOf members by inspecting their discriminator property + for (String memberName : cm.oneOf) { + Map mapped = new HashMap<>(); + mapped.put("modelName", memberName); + // Try to find the discriminator value from the member model's literal property + String discValue = findDiscriminatorValue(memberName, cm.discriminator.getPropertyBaseName(), result); + mapped.put("mappingName", discValue != null ? discValue : memberName); + mappedTypes.add(mapped); + } + } + cm.vendorExtensions.put("x-mapped-models", mappedTypes); + } + + // Detect non-discriminated oneOf unions from the original OAS spec + if (cm.discriminator == null && !cm.vendorExtensions.containsKey("x-is-discriminated-union")) { + if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { + cm.vendorExtensions.put("x-is-union", true); + + // Collect member classnames + List memberNames = new ArrayList<>(); + for (Object memberObj : origSchema.getOneOf()) { + Schema memberSchema = (Schema) memberObj; + String ref = memberSchema.get$ref(); + if (ref != null) { + memberNames.add(toModelName(ModelUtils.getSimpleRef(ref))); + } + } + + // Collect property sets per member to find distinguishing properties + Map> memberPropSets = new LinkedHashMap<>(); + for (String memberName : memberNames) { + Set props = new LinkedHashSet<>(); + ModelsMap memberModels = result.get(memberName); + if (memberModels != null) { + for (ModelMap mm : memberModels.getModels()) { + for (CodegenProperty prop : mm.getModel().vars) { + props.add(prop.name); + } + } + } + memberPropSets.put(memberName, props); + } + + // Build x-union-members list with distinguishing properties + List> members = new ArrayList<>(); + for (int i = 0; i < memberNames.size(); i++) { + String memberName = memberNames.get(i); + Map info = new HashMap<>(); + info.put("modelName", memberName); + + if (i < memberNames.size() - 1) { + // Find a property unique to this member + Set myProps = memberPropSets.getOrDefault(memberName, Collections.emptySet()); + for (String prop : myProps) { + boolean unique = true; + for (Map.Entry> other : memberPropSets.entrySet()) { + if (!other.getKey().equals(memberName) && other.getValue().contains(prop)) { + unique = false; + break; + } + } + if (unique) { + info.put("hasDistinguishingProperty", true); + info.put("distinguishingProperty", prop); + break; + } + } + } + // Last member is always the fallback (no distinguishing property needed) + members.add(info); + } + + cm.vendorExtensions.put("x-union-members", members); + } + } + + // Detect allOf intersection types + if (origSchema != null && origSchema.getAllOf() != null && !origSchema.getAllOf().isEmpty()) { + List parentNames = new ArrayList<>(); + for (Object member : origSchema.getAllOf()) { + Schema memberSchema = (Schema) member; + if (memberSchema.get$ref() != null) { + parentNames.add(toModelName(ModelUtils.getSimpleRef(memberSchema.get$ref()))); + } + } + + if (!parentNames.isEmpty()) { + cm.vendorExtensions.put("x-is-intersection", true); + + List> parents = new ArrayList<>(); + Set parentPropertyNames = new HashSet<>(); + for (String parentName : parentNames) { + Map parentInfo = new HashMap<>(); + parentInfo.put("modelName", parentName); + parents.add(parentInfo); + + ModelsMap parentModels = result.get(parentName); + if (parentModels != null) { + for (ModelMap pmm : parentModels.getModels()) { + for (CodegenProperty prop : pmm.getModel().vars) { + parentPropertyNames.add(prop.name); + } + } + } + } + cm.vendorExtensions.put("x-intersection-parents", parents); + + boolean hasOwnProperties = false; + for (CodegenProperty prop : cm.vars) { + if (!parentPropertyNames.contains(prop.name)) { + prop.vendorExtensions.put("x-is-own-property", true); + hasOwnProperties = true; + } + } + cm.vendorExtensions.put("x-has-own-properties", hasOwnProperties); + } + } + + // Check if model has properties that reference other object models + // (not enums - enums are identity-mapped and don't need DTO mappers) + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null && !prop.isArray) { + if (enumModelNames.contains(prop.complexType)) { + // Reference to an enum type - mark for DTO template to use types. prefix + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.complexType); + } else { + // Reference to an object type - needs DTO mapper + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.complexType); + } + } else if (prop.isArray && prop.items != null && prop.items.complexType != null) { + if (enumModelNames.contains(prop.items.complexType)) { + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.items.complexType); + } else { + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.items.complexType); + } + } + } + + // Add DTO-related vendor extensions + cm.vendorExtensions.put("x-dto-name", cm.classname + "Dto"); + cm.vendorExtensions.put("x-schema-name", cm.classname + "Schema"); + + // Check if has single-value enum properties (literal types) + for (CodegenProperty prop : cm.vars) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + prop.vendorExtensions.put("x-is-literal", true); + prop.vendorExtensions.put("x-literal-value", values.get(0).toString()); + } + } + } + } + } + + return result; + } + + /** + * Find the discriminator property's literal value in a member model. + * E.g., for UserLoginPrincipal with discriminator "kind", returns "USER_LOGIN". + */ + private String findDiscriminatorValue(String modelName, String discriminatorProperty, + Map allModels) { + ModelsMap modelsMap = allModels.get(modelName); + if (modelsMap == null) return null; + + for (ModelMap mo : modelsMap.getModels()) { + CodegenModel cm = mo.getModel(); + for (CodegenProperty prop : cm.vars) { + if (prop.baseName.equals(discriminatorProperty) || prop.name.equals(discriminatorProperty)) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + return values.get(0).toString(); + } + } + } + } + } + return null; + } + + /** + * Add Zod-specific vendor extensions to a model property. + */ + private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { + String zodType = buildZodPropertyType(prop); + prop.vendorExtensions.put("x-zod-type", zodType); + + // Full expression including optional + String fullZod = zodType; + if (!prop.required) { + // Check if the zodType already ends with a schema reference + if (zodType.endsWith("Schema")) { + fullZod = zodType + ".optional()"; + } else { + fullZod = zodType + ".optional()"; + } + } + prop.vendorExtensions.put("x-zod-full", fullZod); + } + + /** + * Build a Zod type expression for a model property. + */ + private String buildZodPropertyType(CodegenProperty prop) { + // Handle enum properties with allowable values + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + // Single-value enum = literal + return "z.literal(\"" + values.get(0) + "\")"; + } + // Multi-value enum — reference the enum schema or inline it + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + // Inline enum + if (values != null) { + StringBuilder sb = new StringBuilder("z.enum(["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("\"").append(values.get(i)).append("\""); + } + sb.append("])"); + return sb.toString(); + } + } + + // Handle arrays (before complexType check to handle Array) + if (prop.isArray && prop.items != null) { + String itemZod = buildZodPropertyType(prop.items); + return itemZod + ".array()"; + } + + // Handle $ref to another model or enum type + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + + // Handle primitive types + if (prop.isInteger || prop.isLong) { + StringBuilder sb = new StringBuilder("z.coerce.number().int()"); + if (prop.minimum != null) { + sb.append(".gte(").append(prop.minimum).append(")"); + } + if (prop.maximum != null) { + sb.append(".lte(").append(prop.maximum).append(")"); + } + return sb.toString(); + } + + if (prop.isNumber || prop.isFloat || prop.isDouble) { + StringBuilder sb = new StringBuilder("z.number()"); + if (prop.minimum != null) { + sb.append(".gte(").append(prop.minimum).append(")"); + } + if (prop.maximum != null) { + sb.append(".lte(").append(prop.maximum).append(")"); + } + return sb.toString(); + } + + if (prop.isBoolean) { + return "z.boolean()"; + } + + if (prop.isString) { + StringBuilder sb = new StringBuilder("z.string()"); + if (prop.maxLength != null) { + sb.append(".max(").append(prop.maxLength).append(")"); + } + if (prop.minLength != null) { + sb.append(".min(").append(prop.minLength).append(")"); + } + return sb.toString(); + } + + // Fallback + return "z.any()"; + } + + @Override + @SuppressWarnings("unchecked") + public Map postProcessSupportingFileData(Map bundle) { + bundle = super.postProcessSupportingFileData(bundle); + + // Build service map from operations for the types template + List allOps = ((org.openapitools.codegen.model.ApiInfoMap) bundle.get("apiInfo")).getApis(); + + // Collect all unique services + Map>> serviceMap = new LinkedHashMap<>(); + // Collect all unique paths for router + Map>> pathMap = new LinkedHashMap<>(); + + for (OperationsMap opsMap : allOps) { + OperationMap operationMap = opsMap.getOperations(); + List ops = operationMap.getOperation(); + + for (CodegenOperation op : ops) { + String serviceName = (String) op.vendorExtensions.getOrDefault("x-service-name", "DefaultService"); + String serviceGetter = (String) op.vendorExtensions.getOrDefault("x-service-getter", "getDefaultService"); + + serviceMap.computeIfAbsent(serviceName, k -> new ArrayList<>()); + + Map opInfo = new HashMap<>(); + String operationIdPascal = camelize(op.operationId); + opInfo.put("operationId", op.operationId); + opInfo.put("operationIdPascal", operationIdPascal); + opInfo.put("description", op.summary != null ? op.summary : op.notes); + opInfo.put("path", op.path); + opInfo.put("httpMethod", op.httpMethod.toLowerCase(Locale.ENGLISH)); + opInfo.put("httpMethodUpper", op.httpMethod.toUpperCase(Locale.ENGLISH)); + opInfo.put("handlerName", op.vendorExtensions.get("x-handler-name")); + opInfo.put("serviceName", serviceName); + opInfo.put("serviceGetter", serviceGetter); + opInfo.put("returnType", op.returnType); + opInfo.put("hasParams", !op.allParams.isEmpty()); + opInfo.put("hasNoParams", op.allParams.isEmpty()); + opInfo.put("allParamsOptional", op.vendorExtensions.get("x-all-params-optional")); + + // Build params info + List> pathParams = new ArrayList<>(); + for (CodegenParameter p : op.pathParams) { + Map pm = new HashMap<>(); + pm.put("name", p.paramName); + pm.put("baseName", p.baseName); + pm.put("zodType", buildZodType(p)); + pathParams.add(pm); + } + opInfo.put("pathParams", pathParams); + opInfo.put("hasPathParams", !pathParams.isEmpty()); + + List> queryParams = new ArrayList<>(); + for (CodegenParameter p : op.queryParams) { + Map qm = new HashMap<>(); + qm.put("name", p.paramName); + qm.put("baseName", p.baseName); + qm.put("hasDifferentName", !p.baseName.equals(p.paramName)); + qm.put("zodType", buildZodType(p)); + qm.put("dataType", p.dataType); + qm.put("required", p.required); + qm.put("isEnum", p.isEnum); + queryParams.add(qm); + } + opInfo.put("queryParams", queryParams); + opInfo.put("hasQueryParams", !queryParams.isEmpty()); + + if (op.bodyParam != null) { + Map body = new HashMap<>(); + body.put("name", op.vendorExtensions.get("x-body-param-name")); + body.put("dataType", op.bodyParam.dataType); + body.put("mapper", op.vendorExtensions.get("x-body-mapper")); + opInfo.put("bodyParam", body); + opInfo.put("hasBodyParam", true); + } else { + opInfo.put("hasBodyParam", false); + } + + // Auth permission from x-auth-permission vendor extension + opInfo.put("permission", op.vendorExtensions.get("x-auth-permission")); + + // Response info + opInfo.put("responseType", op.returnType); + opInfo.put("responseSchema", op.vendorExtensions.get("x-response-schema")); + opInfo.put("responseDtoMapper", op.vendorExtensions.get("x-response-dto-mapper")); + + // SSE detection + boolean hasSSE = false; + for (CodegenResponse response : op.responses) { + if (response.is2xx && response.getContent() != null) { + CodegenMediaType sseMt = response.getContent().get("text/event-stream"); + if (sseMt != null && sseMt.getSchema() != null) { + hasSSE = true; + String sseType = sseMt.getSchema().getComplexType(); + if (sseType == null) sseType = sseMt.getSchema().getDataType(); + opInfo.put("sseType", sseType); + opInfo.put("sseDtoType", sseType + "Dto"); + opInfo.put("sseDtoMapper", "mapTo" + sseType + "Dto"); + opInfo.put("sseSchema", sseType + "Schema"); + break; + } + } + } + opInfo.put("hasSSE", hasSSE); + + // RequestHandler type generics + opInfo.put("paramsGeneric", op.vendorExtensions.get("x-params-generic")); + opInfo.put("queryGeneric", op.vendorExtensions.get("x-query-generic")); + opInfo.put("bodyGeneric", op.vendorExtensions.get("x-body-generic")); + opInfo.put("responseGeneric", op.vendorExtensions.get("x-response-generic")); + + serviceMap.get(serviceName).add(opInfo); + + // Group by path for router + String expressPath = op.path; + pathMap.computeIfAbsent(expressPath, k -> new ArrayList<>()); + pathMap.get(expressPath).add(opInfo); + } + } + + // Convert service map to list for Mustache iteration + List> services = new ArrayList<>(); + for (Map.Entry>> entry : serviceMap.entrySet()) { + Map svc = new HashMap<>(); + svc.put("name", entry.getKey()); + svc.put("getter", "get" + entry.getKey()); + svc.put("operations", entry.getValue()); + services.add(svc); + } + bundle.put("services", services); + + // Convert path map to list for router template + List> routes = new ArrayList<>(); + for (Map.Entry>> entry : pathMap.entrySet()) { + Map route = new HashMap<>(); + route.put("path", entry.getKey()); + route.put("operations", entry.getValue()); + + // Build the allowed methods list + Set methods = new TreeSet<>(); + methods.add("OPTIONS"); + for (Map opInfo : entry.getValue()) { + String method = ((String) opInfo.get("httpMethodUpper")); + methods.add(method); + if ("GET".equals(method)) methods.add("HEAD"); + } + route.put("allowedMethods", String.join(", ", methods)); + routes.add(route); + } + + // Sort routes: most specific (longer, more segments) first to avoid Express routing conflicts + routes.sort((a, b) -> { + String pathA = (String) a.get("path"); + String pathB = (String) b.get("path"); + // Count segments + long segA = pathA.chars().filter(c -> c == '/').count(); + long segB = pathB.chars().filter(c -> c == '/').count(); + if (segA != segB) return Long.compare(segB, segA); // more segments first + // Prefer static segments over parameterized ones + boolean aHasParam = pathA.contains(":"); + boolean bHasParam = pathB.contains(":"); + if (aHasParam != bHasParam) return aHasParam ? 1 : -1; // static first at same depth + return pathA.compareTo(pathB); + }); + + bundle.put("routes", routes); + + // Collect all operations flat for handlers template + List> allOperations = new ArrayList<>(); + for (Map svc : services) { + @SuppressWarnings("unchecked") + List> ops = (List>) svc.get("operations"); + allOperations.addAll(ops); + } + // Sort operations alphabetically by operationId + allOperations.sort(Comparator.comparing(o -> (String) o.get("operationId"))); + bundle.put("allOperations", allOperations); + + // Build auth operations list for auth-middleware template + List> authOperations = allOperations.stream() + .filter(op -> op.get("permission") != null) + .collect(Collectors.toList()); + bundle.put("authOperations", authOperations); + bundle.put("hasAuthOperations", !authOperations.isEmpty()); + + // Topologically sort models so that referenced schemas are declared before they are used + @SuppressWarnings("unchecked") + List models = (List) bundle.get("models"); + if (models != null) { + bundle.put("models", topologicallySortModels(models)); + } + + return bundle; + } + + /** + * Topologically sort models so that leaf types (enums, simple objects) come before + * models that reference them. This ensures correct declaration order in generated schemas. + */ + @SuppressWarnings("unchecked") + private List topologicallySortModels(List models) { + // Build maps from classname to model entry and dependencies + Map modelByName = new LinkedHashMap<>(); + Map> dependencies = new LinkedHashMap<>(); + + for (Object modelEntry : models) { + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm == null) continue; + + modelByName.put(cm.classname, modelEntry); + + Set deps = new LinkedHashSet<>(); + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null) { + deps.add(prop.complexType); + } + if (prop.isArray && prop.items != null && prop.items.complexType != null) { + deps.add(prop.items.complexType); + } + } + // oneOf dependencies + if (cm.oneOf != null) { + deps.addAll(cm.oneOf); + } + // Also include non-discriminated union member dependencies + if (cm.vendorExtensions.containsKey("x-union-members")) { + @SuppressWarnings("unchecked") + List> unionMembers = + (List>) cm.vendorExtensions.get("x-union-members"); + for (Map member : unionMembers) { + deps.add((String) member.get("modelName")); + } + } + // Also include allOf intersection parent dependencies + if (cm.vendorExtensions.containsKey("x-intersection-parents")) { + @SuppressWarnings("unchecked") + List> intersectionParents = + (List>) cm.vendorExtensions.get("x-intersection-parents"); + for (Map parent : intersectionParents) { + deps.add(parent.get("modelName")); + } + } + // Remove self-references + deps.remove(cm.classname); + dependencies.put(cm.classname, deps); + } + + // DFS-based topological sort + List sorted = new ArrayList<>(); + Set visited = new LinkedHashSet<>(); + Set visiting = new LinkedHashSet<>(); + + for (String name : modelByName.keySet()) { + if (!visited.contains(name)) { + topoVisit(name, modelByName, dependencies, visited, visiting, sorted); + } + } + + return sorted; + } + + private void topoVisit(String name, Map modelByName, + Map> dependencies, + Set visited, Set visiting, List sorted) { + if (visited.contains(name)) return; + if (visiting.contains(name)) return; // cycle detected, break it + visiting.add(name); + + Set deps = dependencies.getOrDefault(name, Collections.emptySet()); + for (String dep : deps) { + if (modelByName.containsKey(dep)) { + topoVisit(dep, modelByName, dependencies, visited, visiting, sorted); + } + } + + visiting.remove(name); + visited.add(name); + if (modelByName.containsKey(name)) { + sorted.add(modelByName.get(name)); + } + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 57a8381c0c22..105f2274f76e 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -166,3 +166,6 @@ org.openapitools.codegen.languages.TypeScriptRxjsClientCodegen org.openapitools.codegen.languages.WsdlSchemaCodegen org.openapitools.codegen.languages.XojoClientCodegen org.openapitools.codegen.languages.ZapierClientCodegen + +org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen +org.openapitools.codegen.languages.TypescriptExpressZodClientCodegen diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache @@ -0,0 +1 @@ + diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache new file mode 100644 index 000000000000..7ad36428001a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache @@ -0,0 +1,122 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type * as types from "../types"; +import type * as dtos from "./types"; + +function compact(obj: T): T { + return Object.keys(obj).reduce((acc, key) => { + const value = obj[key as keyof T]; + if (value !== undefined) { + acc[key as keyof T] = value; + } + return acc; + }, {} as T); +} + +{{#models}} +{{#model}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{classname}} { + switch (dto.{{{vendorExtensions.x-discriminator-property}}}) { +{{#vendorExtensions.x-mapped-models}} + case "{{mappingName}}": + return mapFrom{{modelName}}Dto(dto); +{{/vendorExtensions.x-mapped-models}} + } +} + +export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classname}}Dto { + switch (obj.{{{vendorExtensions.x-discriminator-property}}}) { +{{#vendorExtensions.x-mapped-models}} + case "{{mappingName}}": + return mapTo{{modelName}}Dto(obj); +{{/vendorExtensions.x-mapped-models}} + } +} + +{{/vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{classname}} { +{{#vendorExtensions.x-union-members}} +{{#hasDistinguishingProperty}} + if ('{{distinguishingProperty}}' in dto) return mapFrom{{modelName}}Dto(dto as dtos.{{modelName}}Dto); +{{/hasDistinguishingProperty}} +{{^hasDistinguishingProperty}} + return mapFrom{{modelName}}Dto(dto as dtos.{{modelName}}Dto); +{{/hasDistinguishingProperty}} +{{/vendorExtensions.x-union-members}} +} + +export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classname}}Dto { +{{#vendorExtensions.x-union-members}} +{{#hasDistinguishingProperty}} + if ('{{distinguishingProperty}}' in obj) return mapTo{{modelName}}Dto(obj as types.{{modelName}}); +{{/hasDistinguishingProperty}} +{{^hasDistinguishingProperty}} + return mapTo{{modelName}}Dto(obj as any); +{{/hasDistinguishingProperty}} +{{/vendorExtensions.x-union-members}} +} + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-union}} +{{#hasVars}} +export function mapFrom{{classname}}Dto( + dto: dtos.{{classname}}Dto, +): types.{{classname}} { + return compact({ +{{#vars}} +{{#vendorExtensions.x-is-ref}} +{{#isArray}} + {{name}}: dto.{{name}}?.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), +{{/isArray}} +{{^isArray}} + {{name}}: + typeof dto.{{name}} === "undefined" + ? dto.{{name}} + : mapFrom{{vendorExtensions.x-ref-type}}Dto(dto.{{name}}), +{{/isArray}} +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}: dto.{{name}}, +{{/vendorExtensions.x-is-ref}} +{{/vars}} + }); +} + +export function mapTo{{classname}}Dto( + obj: types.{{classname}}, +): dtos.{{classname}}Dto { + return compact({ +{{#vars}} +{{#vendorExtensions.x-is-ref}} +{{#isArray}} + {{name}}: obj.{{name}}?.map(mapTo{{vendorExtensions.x-ref-type}}Dto), +{{/isArray}} +{{^isArray}} + {{name}}: + typeof obj.{{name}} === "undefined" + ? obj.{{name}} + : mapTo{{vendorExtensions.x-ref-type}}Dto(obj.{{name}}), +{{/isArray}} +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}: obj.{{name}}, +{{/vendorExtensions.x-is-ref}} +{{/vars}} + }); +} + +{{/hasVars}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache new file mode 100644 index 000000000000..51ecd3874d76 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache @@ -0,0 +1,119 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type * as types from "../types"; + +{{#models}} +{{#model}} +{{#isEnum}} +{{! Enums don't need a DTO - they're used directly from the types module }} +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = {{#vendorExtensions.x-mapped-models}}{{modelName}}Dto{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; + +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = {{#vendorExtensions.x-union-members}}{{modelName}}Dto{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +{{#vendorExtensions.x-has-own-properties}} +export type {{classname}}Dto = { +{{#vars}} +{{#vendorExtensions.x-is-own-property}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} +{{#vendorExtensions.x-is-enum-ref}} +{{#isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; +{{/isArray}} +{{^isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{^vendorExtensions.x-is-enum-ref}} +{{#isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{^isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{/vendorExtensions.x-is-literal}} +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}{{#vendorExtensions.x-intersection-parents}} & {{modelName}}Dto{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} +{{^vendorExtensions.x-has-own-properties}} +export type {{classname}}Dto = {{#vendorExtensions.x-intersection-parents}}{{modelName}}Dto{{^-last}} & {{/-last}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#hasVars}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = { +{{#vars}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} +{{#vendorExtensions.x-is-enum-ref}} +{{#isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; +{{/isArray}} +{{^isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{^vendorExtensions.x-is-enum-ref}} +{{#isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{^isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{/vendorExtensions.x-is-literal}} +{{/vars}} +}; + +{{/hasVars}} +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache new file mode 100644 index 000000000000..cd6debf2fcdd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache @@ -0,0 +1,218 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import * as types from "./types"; +import * as dtos from "./dtos/types"; +import * as mappers from "./dtos/mappers"; +import * as schemas from "./schemas"; +import { ZodError } from "zod"; + +/** Minimal fetch-like interface for making HTTP requests. */ +export interface FetchLike { + (resource: string, init?: { + method?: string; + headers?: Record; + body?: string; + }): Promise<{ json(): Promise; status: number }>; +} + +{{#hasAnySSE}} +/** Fetch-like interface for SSE responses. Returns raw response with readable body stream. */ +export interface StreamFetchLike { + (resource: string, init?: { + method?: string; + headers?: Record; + body?: string; + }): Promise<{ body: ReadableStream | null; status: number }>; +} + +{{/hasAnySSE}} +/** Options for configuring an HTTP client. */ +export interface ClientOptions { + /** Base URL prefix prepended to all request paths (e.g. "https://api.example.com"). */ + root?: string; + /** Custom mapper for Zod validation errors on the response. */ + mapZodError?: (err: ZodError) => types.ModelError[]; + /** Custom mapper for unhandled exceptions. */ + mapUnhandledException?: (err: unknown) => types.ModelError[]; +{{#hasAnySSE}} + /** Fetch function for SSE streaming. Typically the same fetch used for regular requests. */ + streamFetch?: StreamFetchLike; +{{/hasAnySSE}} +} + +function defaultMapZodError(err: ZodError): types.ModelError[] { + return err.issues.map((issue) => ({ + title: issue.message, + code: issue.code, + source: { pointer: issue.path.join("/") }, + })); +} + +function defaultMapUnhandledException(err: unknown): types.ModelError[] { + const message = err instanceof Error ? err.message : String(err); + return [{ title: message }]; +} + +{{#hasAnySSE}} +async function* parseSSEStream( + body: ReadableStream, + schema: { parse(data: unknown): TDto }, + mapper: (dto: TDto) => TDomain, +): AsyncGenerator { + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n\n"); + buffer = parts.pop() ?? ""; + for (const part of parts) { + if (!part.trim()) continue; + const fields: Record = {}; + for (const line of part.split("\n")) { + const colonIdx = line.indexOf(":"); + if (colonIdx === -1) continue; + const key = line.slice(0, colonIdx).trim(); + const val = line.slice(colonIdx + 1).trim(); + fields[key] = val; + } + if (!fields.data) continue; + try { + const envelope = { + id: fields.id ?? "", + event: fields.event ?? "", + data: JSON.parse(fields.data), + }; + yield mapper(schema.parse(envelope)); + } catch { /* skip malformed events */ } + } + } + } finally { + reader.releaseLock(); + } +} + +{{/hasAnySSE}} +{{#services}} +/** HTTP client for the {{name}} */ +export class Http{{name}} implements types.{{name}} { + private fetch: FetchLike; + private root: string; + private mapZodError: (err: ZodError) => types.ModelError[]; + private mapUnhandledException: (err: unknown) => types.ModelError[]; +{{#hasSSE}} + private streamFetch?: StreamFetchLike; +{{/hasSSE}} + + constructor(fetch: FetchLike, options?: ClientOptions) { + this.fetch = fetch; + this.root = options?.root ?? ""; + this.mapZodError = options?.mapZodError ?? defaultMapZodError; + this.mapUnhandledException = options?.mapUnhandledException ?? defaultMapUnhandledException; +{{#hasSSE}} + this.streamFetch = options?.streamFetch; +{{/hasSSE}} + } + +{{#operations}} + /** {{description}} */ + async {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}> { +{{#hasQueryParams}} + const query = new URLSearchParams(); +{{#queryParams}} +{{#hasDifferentName}} + if (params{{#allParamsOptional}}?{{/allParamsOptional}}.{{name}} !== undefined) query.set("{{baseName}}", String(params{{#allParamsOptional}}!{{/allParamsOptional}}.{{name}})); +{{/hasDifferentName}} +{{^hasDifferentName}} + if (params{{#allParamsOptional}}?{{/allParamsOptional}}.{{name}} !== undefined) query.set("{{name}}", String(params{{#allParamsOptional}}!{{/allParamsOptional}}.{{name}})); +{{/hasDifferentName}} +{{/queryParams}} + const queryString = query.toString(); + const url = `${this.root}{{{urlTemplate}}}${queryString ? `?${queryString}` : ""}`; +{{/hasQueryParams}} +{{^hasQueryParams}} + const url = `${this.root}{{{urlTemplate}}}`; +{{/hasQueryParams}} + try { +{{#hasBodyParam}} + const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url, { +{{#hasFetchMethod}} + method: "{{fetchMethod}}", +{{/hasFetchMethod}} + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params.{{bodyParam.name}}), + }); +{{/hasBodyParam}} +{{^hasBodyParam}} +{{#hasFetchMethod}} + const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url, { method: "{{fetchMethod}}" }); +{{/hasFetchMethod}} +{{^hasFetchMethod}} + const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url); +{{/hasFetchMethod}} +{{/hasBodyParam}} +{{#responseType}} + const json = await response.json(); + return mappers.{{responseDtoMapper}}(schemas.{{responseSchema}}.parse(json)); +{{/responseType}} +{{^responseType}} + await response.json(); +{{/responseType}} + } catch (err) { + if (err instanceof ZodError) { + return { errors: this.mapZodError(err) } as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + } + return { errors: this.mapUnhandledException(err) } as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + } + } + +{{#hasSSE}} + /** {{description}} (streaming) */ + async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator { +{{#hasQueryParams}} + const query = new URLSearchParams(); +{{#queryParams}} +{{#hasDifferentName}} + if (params{{#allParamsOptional}}?{{/allParamsOptional}}.{{name}} !== undefined) query.set("{{baseName}}", String(params{{#allParamsOptional}}!{{/allParamsOptional}}.{{name}})); +{{/hasDifferentName}} +{{^hasDifferentName}} + if (params{{#allParamsOptional}}?{{/allParamsOptional}}.{{name}} !== undefined) query.set("{{name}}", String(params{{#allParamsOptional}}!{{/allParamsOptional}}.{{name}})); +{{/hasDifferentName}} +{{/queryParams}} + const queryString = query.toString(); + const url = `${this.root}{{{urlTemplate}}}${queryString ? `?${queryString}` : ""}`; +{{/hasQueryParams}} +{{^hasQueryParams}} + const url = `${this.root}{{{urlTemplate}}}`; +{{/hasQueryParams}} + const fetchFn = this.streamFetch ?? (this.fetch as unknown as StreamFetchLike); + const response = await fetchFn(url, { +{{#hasFetchMethod}} + method: "{{fetchMethod}}", +{{/hasFetchMethod}} +{{#hasBodyParam}} + headers: { "Content-Type": "application/json", "Accept": "text/event-stream" }, + body: JSON.stringify(params.{{bodyParam.name}}), +{{/hasBodyParam}} +{{^hasBodyParam}} + headers: { "Accept": "text/event-stream" }, +{{/hasBodyParam}} + }); + const body = (response as any).body as ReadableStream | null; + if (!body) throw new Error("Response body not available for SSE streaming"); + yield* parseSSEStream(body, schemas.{{sseSchema}}, mappers.{{sseDtoMapper}}); + } + +{{/hasSSE}} +{{/operations}} +} + +{{/services}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache new file mode 100644 index 000000000000..ced75f4752b5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache @@ -0,0 +1,9 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +export * from "./types"; +export * from "./http-client"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache @@ -0,0 +1 @@ + diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache new file mode 100644 index 000000000000..921b8027762c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache @@ -0,0 +1,59 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import { z } from "zod"; + +{{#models}} +{{#model}} +{{#isEnum}} +export const {{classname}}Schema = z.enum([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]); + +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +export const {{classname}}Schema = z.discriminatedUnion("{{{vendorExtensions.x-discriminator-property}}}", [ +{{#vendorExtensions.x-mapped-models}} + {{modelName}}Schema, +{{/vendorExtensions.x-mapped-models}} +]); + +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +export const {{classname}}Schema = z.union([ +{{#vendorExtensions.x-union-members}} + {{modelName}}Schema, +{{/vendorExtensions.x-union-members}} +]); + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +export const {{classname}}Schema = z.object({ +{{#vars}} +{{#vendorExtensions.x-is-own-property}} + {{name}}: {{{vendorExtensions.x-zod-full}}}, +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}){{#vendorExtensions.x-intersection-parents}}.merge({{modelName}}Schema){{/vendorExtensions.x-intersection-parents}}; + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#hasVars}} +export const {{classname}}Schema = z.object({ +{{#vars}} + {{name}}: {{{vendorExtensions.x-zod-full}}}, +{{/vars}} +}); + +{{/hasVars}} +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache new file mode 100644 index 000000000000..8b7862a084bf --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache @@ -0,0 +1,121 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +{{#services}} +/** Interface for the {{name}} */ +export interface {{name}} { +{{#operations}} + /** {{description}} */ + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}>; + +{{#hasSSE}} + /** {{description}} (streaming) */ + {{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): AsyncIterable<{{sseType}}>; + +{{/hasSSE}} +{{/operations}} +} + +{{/services}} +{{#allOperations}} +{{#hasParams}} +export type {{operationIdPascal}}Params = { +{{#pathParams}} + /** {{baseName}} */ + {{name}}: string; +{{/pathParams}} +{{#queryParams}} + /** {{baseName}} */ + {{name}}{{^required}}?{{/required}}: {{dataType}}; +{{/queryParams}} +{{#hasBodyParam}} + /** {{bodyParam.name}} */ + {{bodyParam.name}}: {{bodyParam.dataType}}; +{{/hasBodyParam}} +}; + +{{/hasParams}} +{{/allOperations}} +{{#models}} +{{#model}} +{{#isEnum}} +export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}}; + +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = {{#vendorExtensions.x-mapped-models}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; + +{{#vendorExtensions.x-mapped-models}} +export function is{{modelName}}(obj: {{classname}}): obj is {{modelName}} { + return obj.{{{vendorExtensions.x-discriminator-property}}} === "{{mappingName}}"; +} + +{{/vendorExtensions.x-mapped-models}} +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +{{#description}} +/** {{.}} */ +{{/description}} +{{#vendorExtensions.x-has-own-properties}} +export type {{classname}} = { +{{#vars}} +{{#vendorExtensions.x-is-own-property}} +{{#description}} + /** {{.}} */ +{{/description}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-literal}} +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}{{#vendorExtensions.x-intersection-parents}} & {{modelName}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} +{{^vendorExtensions.x-has-own-properties}} +export type {{classname}} = {{#vendorExtensions.x-intersection-parents}}{{modelName}}{{^-last}} & {{/-last}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = { +{{#vars}} +{{#description}} + /** {{.}} */ +{{/description}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-literal}} +{{/vars}} +}; + +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/api.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/api.mustache new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache new file mode 100644 index 000000000000..6d7b2e644782 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache @@ -0,0 +1,122 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type * as types from "../types"; +import type * as dtos from "./types"; + +function compact(obj: T): T { + return Object.keys(obj).reduce((acc, key) => { + const value = obj[key as keyof T]; + if (value !== undefined) { + acc[key as keyof T] = value; + } + return acc; + }, {} as T); +} + +{{#models}} +{{#model}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{classname}} { + switch (dto.{{{vendorExtensions.x-discriminator-property}}}) { +{{#vendorExtensions.x-mapped-models}} + case "{{mappingName}}": + return mapFrom{{modelName}}Dto(dto); +{{/vendorExtensions.x-mapped-models}} + } +} + +export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classname}}Dto { + switch (obj.{{{vendorExtensions.x-discriminator-property}}}) { +{{#vendorExtensions.x-mapped-models}} + case "{{mappingName}}": + return mapTo{{modelName}}Dto(obj); +{{/vendorExtensions.x-mapped-models}} + } +} + +{{/vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{classname}} { +{{#vendorExtensions.x-union-members}} +{{#hasDistinguishingProperty}} + if ('{{distinguishingProperty}}' in dto) return mapFrom{{modelName}}Dto(dto as dtos.{{modelName}}Dto); +{{/hasDistinguishingProperty}} +{{^hasDistinguishingProperty}} + return mapFrom{{modelName}}Dto(dto as dtos.{{modelName}}Dto); +{{/hasDistinguishingProperty}} +{{/vendorExtensions.x-union-members}} +} + +export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classname}}Dto { +{{#vendorExtensions.x-union-members}} +{{#hasDistinguishingProperty}} + if ('{{distinguishingProperty}}' in obj) return mapTo{{modelName}}Dto(obj as types.{{modelName}}); +{{/hasDistinguishingProperty}} +{{^hasDistinguishingProperty}} + return mapTo{{modelName}}Dto(obj as any); +{{/hasDistinguishingProperty}} +{{/vendorExtensions.x-union-members}} +} + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-union}} +{{#hasVars}} +export function mapFrom{{classname}}Dto( + dto: dtos.{{classname}}Dto, +): types.{{classname}} { + return compact({ +{{#vars}} +{{#vendorExtensions.x-is-ref}} +{{#isArray}} + {{name}}: dto.{{name}}?.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), +{{/isArray}} +{{^isArray}} + {{name}}: + typeof dto.{{name}} === "undefined" + ? dto.{{name}} + : mapFrom{{vendorExtensions.x-ref-type}}Dto(dto.{{name}}), +{{/isArray}} +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}: dto.{{name}}, +{{/vendorExtensions.x-is-ref}} +{{/vars}} + }); +} + +export function mapTo{{classname}}Dto( + obj: types.{{classname}}, +): dtos.{{classname}}Dto { + return compact({ +{{#vars}} +{{#vendorExtensions.x-is-ref}} +{{#isArray}} + {{name}}: obj.{{name}}?.map(mapTo{{vendorExtensions.x-ref-type}}Dto), +{{/isArray}} +{{^isArray}} + {{name}}: + typeof obj.{{name}} === "undefined" + ? obj.{{name}} + : mapTo{{vendorExtensions.x-ref-type}}Dto(obj.{{name}}), +{{/isArray}} +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}: obj.{{name}}, +{{/vendorExtensions.x-is-ref}} +{{/vars}} + }); +} + +{{/hasVars}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache new file mode 100644 index 000000000000..71d6708f0c03 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache @@ -0,0 +1,119 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type * as types from "../types"; + +{{#models}} +{{#model}} +{{#isEnum}} +{{! Enums don't need a DTO - they're used directly from the types module }} +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = {{#vendorExtensions.x-mapped-models}}{{modelName}}Dto{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; + +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = {{#vendorExtensions.x-union-members}}{{modelName}}Dto{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +{{#vendorExtensions.x-has-own-properties}} +export type {{classname}}Dto = { +{{#vars}} +{{#vendorExtensions.x-is-own-property}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} +{{#vendorExtensions.x-is-enum-ref}} +{{#isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; +{{/isArray}} +{{^isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{^vendorExtensions.x-is-enum-ref}} +{{#isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{^isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{/vendorExtensions.x-is-literal}} +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}{{#vendorExtensions.x-intersection-parents}} & {{modelName}}Dto{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} +{{^vendorExtensions.x-has-own-properties}} +export type {{classname}}Dto = {{#vendorExtensions.x-intersection-parents}}{{modelName}}Dto{{^-last}} & {{/-last}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#hasVars}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = { +{{#vars}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} +{{#vendorExtensions.x-is-enum-ref}} +{{#isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; +{{/isArray}} +{{^isArray}} + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{^vendorExtensions.x-is-enum-ref}} +{{#isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{^isArray}} +{{#vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; +{{/vendorExtensions.x-is-ref}} +{{^vendorExtensions.x-is-ref}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-ref}} +{{/isArray}} +{{/vendorExtensions.x-is-enum-ref}} +{{/vendorExtensions.x-is-literal}} +{{/vars}} +}; + +{{/hasVars}} +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/auth-middleware.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/auth-middleware.mustache new file mode 100644 index 000000000000..9455ab64e0bc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/auth-middleware.mustache @@ -0,0 +1,58 @@ +{{#hasAuthOperations}} +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type { RequestHandler } from "express"; +import { handledException } from "./errors"; +import type { Middleware } from "./types"; + +export type PermissionGrantResolver = (params: { + role: string; + permission: string; +}) => Promise; + +const requirePermissionGrant = ( + permission: string, + resolver: PermissionGrantResolver +): RequestHandler => { + return async (req, _, next) => { + if (!req.principal) { + return next( + handledException(401, [ + { title: "Unauthorized", status: 401, code: "UNAUTHORIZED" }, + ]) + ); + } + + const role = req.principal.role; + const hasPermission = await resolver({ role, permission }); + + if (hasPermission) { + next(); + } else { + next( + handledException(403, [ + { + title: "Forbidden", + status: 403, + code: "FORBIDDEN", + detail: `You do not have permission to perform this action. Required permission: ${permission}`, + }, + ]) + ); + } + }; +}; + +export const authMiddleware = ( + resolver: PermissionGrantResolver +): Middleware => ({ +{{#authOperations}} + {{operationId}}: requirePermissionGrant("{{permission}}", resolver), +{{/authOperations}} +}); +{{/hasAuthOperations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache new file mode 100644 index 000000000000..8584bd075da2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache @@ -0,0 +1,71 @@ +import type { ZodIssue } from "zod"; + +import type * as types from "../types"; + +export function methodNotAllowed(): MethodNotAllowedError { + return { + code: "METHOD_NOT_ALLOWED", + status: 405, + title: "Method Not Allowed", + }; +} + +export function isMethodNotAllowed(error: any): error is MethodNotAllowedError { + return error.code === "METHOD_NOT_ALLOWED"; +} + +export type MethodNotAllowedError = { + code: "METHOD_NOT_ALLOWED"; + status: number; + title: string; +}; + +export function validationErrors( + status: 400 | 500, + errors: ZodIssue[], +): ValidationErrorsError { + return { code: "VALIDATION_ERRORS", status, errors }; +} + +export function isValidationErrors(error: any): error is ValidationErrorsError { + return error.code === "VALIDATION_ERRORS"; +} + +export type ValidationErrorsError = { + code: "VALIDATION_ERRORS"; + status: number; + errors: ZodIssue[]; +}; + +export function handledException( + status: number, + errors: types.ModelError[], +): HandledExceptionError { + return { code: "HANDLED_EXCEPTION", status, errors }; +} + +export function isHandledException(error: any): error is HandledExceptionError { + return error.code === "HANDLED_EXCEPTION"; +} + +export type HandledExceptionError = { + code: "HANDLED_EXCEPTION"; + status: number; + errors: types.ModelError[]; +}; + +export function unhandledException(exception: any): UnhandledExceptionError { + return { code: "UNHANDLED_EXCEPTION", status: 500, exception }; +} + +export function isUnhandledException( + error: any, +): error is UnhandledExceptionError { + return error.code === "UNHANDLED_EXCEPTION"; +} + +export type UnhandledExceptionError = { + code: "UNHANDLED_EXCEPTION"; + status: number; + exception: any; +}; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache new file mode 100644 index 000000000000..3facd91d43b7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache @@ -0,0 +1,152 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type { Request, Response } from "express"; +import { ZodError } from "zod"; + +import type * as types from "../types"; +import * as schemas from "../schemas"; +import * as mappers from "../dtos/mappers"; + +import * as errors from "./errors"; +import type * as expressTypes from "./types"; + +{{#allOperations}} +/** {{httpMethodUpper}} {{path}} */ +export const {{handlerName}} = + ( + getService: (req: Request, res: Response) => types.{{serviceName}}, + ): expressTypes.{{operationIdPascal}}RequestHandler => + async (req, res, next) => { + try { +{{#hasParams}} + // Parse parameters from request +{{#hasNoParams}} +{{/hasNoParams}} +{{^hasNoParams}} +{{#allParamsOptional}} + const params: types.{{operationIdPascal}}Params | undefined = + schemas.{{operationIdPascal}}ParamsSchema.optional().parse({ +{{/allParamsOptional}} +{{^allParamsOptional}} + const params: types.{{operationIdPascal}}Params = + schemas.{{operationIdPascal}}ParamsSchema.parse({ +{{/allParamsOptional}} +{{#pathParams}} + {{name}}: req.params.{{name}}, +{{/pathParams}} +{{#queryParams}} +{{#hasDifferentName}} + {{name}}: req.query["{{baseName}}"], +{{/hasDifferentName}} +{{^hasDifferentName}} + {{name}}: req.query.{{baseName}}, +{{/hasDifferentName}} +{{/queryParams}} +{{#hasBodyParam}} + {{bodyParam.name}}: mappers.{{bodyParam.mapper}}(req.body), +{{/hasBodyParam}} + }); + +{{/hasNoParams}} +{{/hasParams}} + // Execute service method + const service = getService(req, res); +{{#hasSSE}} + const result = service.{{operationId}}({{#hasParams}}params{{/hasParams}}); + + if (Symbol.asyncIterator in Object(result)) { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + try { + for await (const chunk of result as AsyncIterable) { + const dto = mappers.{{{sseDtoMapper}}}(chunk); + res.write(`id: ${dto.id}\nevent: ${dto.event}\ndata: ${JSON.stringify(dto.data)}\n\n`); + } + } catch (streamErr) { + res.write(`event: error\ndata: ${JSON.stringify({ message: streamErr instanceof Error ? streamErr.message : "Stream error" })}\n\n`); + } finally { + res.end(); + } + } else { + const resolved = await (result as Promise); + const status = getHttpStatus(200, resolved); + if (isError(resolved)) { + next(errors.handledException(status, resolved.errors)); + } else { +{{#responseDtoMapper}} + const responseDto = mappers.{{{responseDtoMapper}}}(resolved); + res.status(status).json(responseDto); +{{/responseDtoMapper}} +{{^responseDtoMapper}} + res.status(status).json(resolved); +{{/responseDtoMapper}} +{{#responseSchema}} + schemas.{{{responseSchema}}}.parse(resolved); +{{/responseSchema}} + } + } +{{/hasSSE}} +{{^hasSSE}} + const result = await service.{{operationId}}({{#hasParams}}params{{/hasParams}}); + const status = getHttpStatus(200, result); + if (isError(result)) { + next(errors.handledException(status, result.errors)); + } else { +{{#responseDtoMapper}} + const responseDto = mappers.{{{responseDtoMapper}}}(result); + res.status(status).json(responseDto); +{{/responseDtoMapper}} +{{^responseDtoMapper}} + res.status(status).json(result); +{{/responseDtoMapper}} +{{#responseSchema}} + schemas.{{{responseSchema}}}.parse(result); +{{/responseSchema}} + } +{{/hasSSE}} + } catch (err) { + if (err instanceof ZodError) { + const statusCode = res.headersSent ? 500 : 400; + return next(errors.validationErrors(statusCode, err.errors)); + } else { + next(errors.unhandledException(err)); + } + } + }; + +{{/allOperations}} +function isError(result: { + errors: { status?: number | string }[]; + data?: any; +}): boolean { + return ( + !!result.errors.length && + (Array.isArray(result.data) + ? result.data.length === 0 + : result.data === undefined) + ); +} + +function getHttpStatus( + success: number, + result: { errors: { status?: number | string }[]; data?: unknown }, +): number { + if (isError(result)) { + return result.errors.reduce((max, item) => { + if (typeof item.status === "undefined") return success; + const value = + typeof item.status === "string" ? Number(item.status) : item.status; + return !Number.isNaN(value) && value > max ? value : max; + }, success); + } else { + return success; + } +} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache new file mode 100644 index 000000000000..cc69218c910d --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache @@ -0,0 +1,5 @@ +export * from "./auth-middleware"; +export * from "./errors"; +export * from "./handlers"; +export * from "./router-factory"; +export * from "./types"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache new file mode 100644 index 000000000000..4dbaad21f079 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache @@ -0,0 +1,147 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import { Router, type RequestHandler } from "express"; + +import * as handlers from "./handlers"; +import * as errors from "./errors"; +import type * as expressTypes from "./types"; + +export function getRouter({ + schema, +{{#services}} + {{getter}}, +{{/services}} + middleware, + handlerOverrides, + swaggerUiVersion = "3.51.1", +}: expressTypes.RouterFactoryInput): Router { + const router = Router(); + const methodNotAllowed: (allow: string) => RequestHandler = + (allow) => (_req, res, next) => { + res.set("Allow", allow); + return next(errors.methodNotAllowed()); + }; + + function handlersFor< + TName extends Exclude< + keyof expressTypes.Middleware, + "_onlySwaggerUI" | "_exceptSwaggerUI" + >, + >(methodName: TName, defaultHandler: RequestHandler): RequestHandler[] { + return [ + ...getMiddleware(middleware, methodName), + ...getHandlers(handlerOverrides?.[methodName], defaultHandler), + ]; + } + +{{#routes}} + router + .route("{{{path}}}") +{{#operations}} + .{{httpMethod}}( + handlersFor( + "{{operationId}}", + handlers.{{handlerName}}({{serviceGetter}}), + ), + ) +{{/operations}} + .options((_, res) => { + res.set("Allow", "{{{allowedMethods}}}").sendStatus(204); + }) + .all(methodNotAllowed("{{{allowedMethods}}}")); + +{{/routes}} + router + .route("/") + .get(...getMiddleware(middleware, "_onlySwaggerUI"), (_, res) => { + const swaggerUiUrl = `https://unpkg.com/swagger-ui-dist@${swaggerUiVersion}/`; + res.send(` + + + + API Documentation + + + + + +
CONTENT NOT LOADED
+ + + + `); + }) + .options((_, res) => { + res.set("Allow", "GET, HEAD, OPTIONS").sendStatus(204); + }) + .all(methodNotAllowed("GET, HEAD, OPTIONS")); + return router; +} +/** + * Gets a list of middleware functions for a given name from the supplied middleware object(s). + * If no middleware objects are supplied, an empty array is returned. + * Handlers are returned in the order they are defined in the middleware object(s). + * @param middleware - The middleware object(s) to extract middleware from + * @param name - The name of the service method to get middleware for + * @returns An array of {@link RequestHandler|express.RequestHandler} functions + */ +function getMiddleware< + TName extends string, + TMiddleware extends Partial< + Record + >, +>( + middleware: TMiddleware | TMiddleware[] | undefined, + name: TName, +): RequestHandler[] { + if (!middleware) return []; + const middlewareArray = Array.isArray(middleware) ? middleware : [middleware]; + + return middlewareArray.flatMap((m) => { + const _exceptSwaggerUI = + m["_exceptSwaggerUI"] && name !== "_onlySwaggerUI" + ? m["_exceptSwaggerUI"] + : []; + + const base = m[name] ? m[name] : []; + const baseArray = Array.isArray(base) ? base : [base]; + return [...baseArray, ..._exceptSwaggerUI]; + }); +} + +/** + * Gets a list of handlers for a given service method. + * If overrides are provided, they are used in place of the default handler. + * If no overrides are provided, the default handler is used. + * @param overrides - The handler(s) to use in place of the default handler + * @param defaultHandler - The default handler to use if no overrides are provided + * @returns An array of {@link RequestHandler|express.RequestHandler} functions + */ +function getHandlers( + overrides: TRequestHandler | TRequestHandler[] | undefined, + defaultHandler: TRequestHandler, +): TRequestHandler[] { + if (!overrides) return [defaultHandler]; + return Array.isArray(overrides) ? overrides : [overrides]; +} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/types.mustache new file mode 100644 index 000000000000..427c52209be4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/types.mustache @@ -0,0 +1,49 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type { Request, Response, RequestHandler } from "express"; + +import type * as types from "../types"; +import type * as dtos from "../dtos/types"; + +export type RouterFactoryInput = { + /** OpenAPI schema as a JSON object */ + schema: any; + +{{#services}} + {{getter}}: ( + req: Request, + res: Response, + ) => types.{{name}}; +{{/services}} + + middleware?: Middleware | Middleware[]; + handlerOverrides?: Middleware; + swaggerUiVersion?: string; +}; + +export type Middleware = { + /** Middleware to be applied to the Swagger UI handler. */ + _onlySwaggerUI?: RequestHandler | RequestHandler[]; + /** Middleware to be applied to all handlers _except_ Swagger UI after any other method-specific middleware. */ + _exceptSwaggerUI?: RequestHandler | RequestHandler[]; +{{#allOperations}} + {{operationId}}?: + | {{operationIdPascal}}RequestHandler + | {{operationIdPascal}}RequestHandler[]; +{{/allOperations}} +}; + +{{#allOperations}} +export type {{operationIdPascal}}RequestHandler = RequestHandler< + {{{paramsGeneric}}}, + {{{responseGeneric}}}, + {{{bodyGeneric}}}, + {{{queryGeneric}}} +>; + +{{/allOperations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/model.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/model.mustache new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache new file mode 100644 index 000000000000..ef5c4fd9c717 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache @@ -0,0 +1,75 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import { z } from "zod"; + +{{#models}} +{{#model}} +{{#isEnum}} +export const {{classname}}Schema = z.enum([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]); + +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +export const {{classname}}Schema = z.discriminatedUnion("{{{vendorExtensions.x-discriminator-property}}}", [ +{{#vendorExtensions.x-mapped-models}} + {{modelName}}Schema, +{{/vendorExtensions.x-mapped-models}} +]); + +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +export const {{classname}}Schema = z.union([ +{{#vendorExtensions.x-union-members}} + {{modelName}}Schema, +{{/vendorExtensions.x-union-members}} +]); + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +export const {{classname}}Schema = z.object({ +{{#vars}} +{{#vendorExtensions.x-is-own-property}} + {{name}}: {{{vendorExtensions.x-zod-full}}}, +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}){{#vendorExtensions.x-intersection-parents}}.merge({{modelName}}Schema){{/vendorExtensions.x-intersection-parents}}; + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#hasVars}} +export const {{classname}}Schema = z.object({ +{{#vars}} + {{name}}: {{{vendorExtensions.x-zod-full}}}, +{{/vars}} +}); + +{{/hasVars}} +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} +{{#allOperations}} +{{#hasParams}} +export const {{operationIdPascal}}ParamsSchema = z.object({ +{{#pathParams}} + {{name}}: {{{zodType}}}, +{{/pathParams}} +{{#queryParams}} + {{name}}: {{{zodType}}}, +{{/queryParams}} +{{#hasBodyParam}} + {{bodyParam.name}}: {{bodyParam.dataType}}Schema, +{{/hasBodyParam}} +}); + +{{/hasParams}} +{{/allOperations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache new file mode 100644 index 000000000000..222529d22704 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache @@ -0,0 +1,121 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +{{#services}} +/** Interface for the {{name}} */ +export interface {{name}} { +{{#operations}} + /** {{description}} */ +{{#hasSSE}} + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}> | AsyncIterable<{{sseType}}>; +{{/hasSSE}} +{{^hasSSE}} + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}>; +{{/hasSSE}} + +{{/operations}} +} + +{{/services}} +{{#allOperations}} +{{#hasParams}} +export type {{operationIdPascal}}Params = { +{{#pathParams}} + /** {{baseName}} */ + {{name}}: string; +{{/pathParams}} +{{#queryParams}} + /** {{baseName}} */ + {{name}}{{^required}}?{{/required}}: {{dataType}}; +{{/queryParams}} +{{#hasBodyParam}} + /** {{bodyParam.name}} */ + {{bodyParam.name}}: {{bodyParam.dataType}}; +{{/hasBodyParam}} +}; + +{{/hasParams}} +{{/allOperations}} +{{#models}} +{{#model}} +{{#isEnum}} +export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}}; + +{{/isEnum}} +{{^isEnum}} +{{#vendorExtensions.x-is-discriminated-union}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = {{#vendorExtensions.x-mapped-models}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; + +{{#vendorExtensions.x-mapped-models}} +export function is{{modelName}}(obj: {{classname}}): obj is {{modelName}} { + return obj.{{{vendorExtensions.x-discriminator-property}}} === "{{mappingName}}"; +} + +{{/vendorExtensions.x-mapped-models}} +{{/vendorExtensions.x-is-discriminated-union}} +{{^vendorExtensions.x-is-discriminated-union}} +{{#vendorExtensions.x-is-union}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; + +{{/vendorExtensions.x-is-union}} +{{^vendorExtensions.x-is-union}} +{{#vendorExtensions.x-is-intersection}} +{{#description}} +/** {{.}} */ +{{/description}} +{{#vendorExtensions.x-has-own-properties}} +export type {{classname}} = { +{{#vars}} +{{#vendorExtensions.x-is-own-property}} +{{#description}} + /** {{.}} */ +{{/description}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-literal}} +{{/vendorExtensions.x-is-own-property}} +{{/vars}} +}{{#vendorExtensions.x-intersection-parents}} & {{modelName}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} +{{^vendorExtensions.x-has-own-properties}} +export type {{classname}} = {{#vendorExtensions.x-intersection-parents}}{{modelName}}{{^-last}} & {{/-last}}{{/vendorExtensions.x-intersection-parents}}; +{{/vendorExtensions.x-has-own-properties}} + +{{/vendorExtensions.x-is-intersection}} +{{^vendorExtensions.x-is-intersection}} +{{#description}} +/** {{.}} */ +{{/description}} +export type {{classname}} = { +{{#vars}} +{{#description}} + /** {{.}} */ +{{/description}} +{{#vendorExtensions.x-is-literal}} + {{name}}: "{{{vendorExtensions.x-literal-value}}}"; +{{/vendorExtensions.x-is-literal}} +{{^vendorExtensions.x-is-literal}} + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; +{{/vendorExtensions.x-is-literal}} +{{/vars}} +}; + +{{/vendorExtensions.x-is-intersection}} +{{/vendorExtensions.x-is-union}} +{{/vendorExtensions.x-is-discriminated-union}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java new file mode 100644 index 000000000000..2a13fcd6b01c --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java @@ -0,0 +1,28 @@ +package org.openapitools.codegen.options; + +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class TypescriptExpressZodServerCodegenOptionsProvider implements OptionsProvider { + public static final String PROJECT_NAME_VALUE = "OpenAPI"; + + @Override + public String getLanguage() { + return "typescript-express-zod-server"; + } + + @Override + public Map createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder.build(); + } + + @Override + public boolean isServer() { + return true; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java new file mode 100644 index 000000000000..1e9058b2137c --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java @@ -0,0 +1,29 @@ +package org.openapitools.codegen.typescript.express.zod; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; +import io.swagger.models.*; +import io.swagger.models.properties.*; + +import org.testng.Assert; +import org.testng.annotations.Test; + +@SuppressWarnings("static-method") +public class TypescriptExpressZodServerCodegenModelTest { + + @Test(description = "convert a simple java model") + public void simpleModelTest() { + final Model model = new ModelImpl() + .description("a sample model") + .property("id", new LongProperty()) + .property("name", new StringProperty()) + .required("id") + .required("name"); + final DefaultCodegen codegen = new TypescriptExpressZodServerCodegen(); + + // TODO: Complete this test. + Assert.fail("Not implemented."); + } + +} + diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java new file mode 100644 index 000000000000..4658f4f4ec90 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java @@ -0,0 +1,30 @@ +package org.openapitools.codegen.typescript.express.zod; + +import org.openapitools.codegen.AbstractOptionsTest; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; +import org.openapitools.codegen.options.TypescriptExpressZodServerCodegenOptionsProvider; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class TypescriptExpressZodServerCodegenOptionsTest extends AbstractOptionsTest { + private TypescriptExpressZodServerCodegen codegen = mock(TypescriptExpressZodServerCodegen.class, mockSettings); + + public TypescriptExpressZodServerCodegenOptionsTest() { + super(new TypescriptExpressZodServerCodegenOptionsProvider()); + } + + @Override + protected CodegenConfig getCodegenConfig() { + return codegen; + } + + @SuppressWarnings("unused") + @Override + protected void verifyOptions() { + // TODO: Complete options using Mockito + // verify(codegen).someMethod(arguments) + } +} + diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java new file mode 100644 index 000000000000..8e487719cb48 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java @@ -0,0 +1,19 @@ +package org.openapitools.codegen.typescript.express.zod; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; +import io.swagger.models.*; +import io.swagger.parser.SwaggerParser; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class TypescriptExpressZodServerCodegenTest { + + TypescriptExpressZodServerCodegen codegen = new TypescriptExpressZodServerCodegen(); + + @Test + public void shouldSucceed() throws Exception { + // TODO: Complete this test. + Assert.fail("Not implemented."); + } +} From 0e3530f4165b38927ddb146a5a013ff7231f91c8 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Thu, 12 Feb 2026 08:08:11 -0500 Subject: [PATCH 02/15] Added exports in index.ts files --- .../languages/TypescriptExpressZodClientCodegen.java | 1 + .../typescript-express-zod-client/dtos/index.mustache | 9 +++++++++ .../typescript-express-zod-client/index.mustache | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java index 64308eb77499..da1b9f4f86bf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java @@ -67,6 +67,7 @@ public void processOpts() { supportingFiles.add(new SupportingFile("schemas.mustache", "", "schemas.ts")); supportingFiles.add(new SupportingFile("dtos" + File.separator + "types.mustache", "dtos", "types.ts")); supportingFiles.add(new SupportingFile("dtos" + File.separator + "mappers.mustache", "dtos", "mappers.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "index.mustache", "dtos", "index.ts")); supportingFiles.add(new SupportingFile("http-client.mustache", "", "http-client.ts")); supportingFiles.add(new SupportingFile("index.mustache", "", "index.ts")); } diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache new file mode 100644 index 000000000000..d3d8b3f9bc71 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache @@ -0,0 +1,9 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-client). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +export * from "./types"; +export * from "./mappers"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache index ced75f4752b5..6c2070a43e85 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache @@ -6,4 +6,6 @@ */ export * from "./types"; +export * from "./schemas"; +export * from "./dtos"; export * from "./http-client"; From 7c438ca6ee8d6fbc146478b5dbfb527057b92cb5 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Thu, 12 Feb 2026 08:50:43 -0500 Subject: [PATCH 03/15] Added index export to typescript-express-zod-server --- .../codegen/languages/TypescriptExpressZodServerCodegen.java | 2 ++ .../typescript-express-zod-server/dtos/index.mustache | 2 ++ .../resources/typescript-express-zod-server/index.mustache | 4 ++++ 3 files changed, 8 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/index.mustache create mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-server/index.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index 1822ea08a3c2..c54ee3f3a98e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -72,6 +72,8 @@ public void processOpts() { supportingFiles.add(new SupportingFile("express" + File.separator + "errors.mustache", "express", "errors.ts")); supportingFiles.add(new SupportingFile("express" + File.separator + "auth-middleware.mustache", "express", "auth-middleware.ts")); supportingFiles.add(new SupportingFile("express" + File.separator + "index.mustache", "express", "index.ts")); + supportingFiles.add(new SupportingFile("dtos" + File.separator + "index.mustache", "dtos", "index.ts")); + supportingFiles.add(new SupportingFile("index.mustache", "", "index.ts")); } @Override diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/index.mustache new file mode 100644 index 000000000000..cfd0927bbd2b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/index.mustache @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./mappers"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/index.mustache new file mode 100644 index 000000000000..019e4c35bb62 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/index.mustache @@ -0,0 +1,4 @@ +export * from "./types"; +export * from "./schemas"; +export * from "./dtos"; +export * from "./express"; From d29b41eedfbd56d4ae3a976000842576590d6bd6 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Thu, 12 Feb 2026 19:55:53 -0500 Subject: [PATCH 04/15] Enhance TypeScript Express Zod server and client generation - Updated Zod to v4 - Update enum handling in Zod schemas to support allowable values. - Make discriminator properties required in Zod schemas for better type inference. - Refactor response headers in handlers for improved SSE support. - Change merge to extend for intersection parents in schemas. - Ensure dataType is correctly formatted in generated types. --- .../TypescriptExpressZodServerCodegen.java | 47 +++++++++++++++++-- .../schemas.mustache | 2 +- .../express/handlers.mustache | 14 +++--- .../schemas.mustache | 2 +- .../types.mustache | 2 +- 5 files changed, 55 insertions(+), 12 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index c54ee3f3a98e..08039818e1ca 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -179,9 +179,19 @@ private void addZodVendorExtensions(CodegenParameter param) { private String buildZodType(CodegenParameter param) { StringBuilder zod = new StringBuilder(); - if (param.isEnum) { - // Reference the enum schema - zod.append(camelize(param.dataType) + "Schema"); + if (param.isEnum && param.allowableValues != null) { + List values = (List) param.allowableValues.get("values"); + if (values != null && !values.isEmpty()) { + StringBuilder sb = new StringBuilder("z.enum(["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("'").append(values.get(i)).append("'"); + } + sb.append("])"); + zod.append(sb); + } else { + zod.append(camelize(param.dataType) + "Schema"); + } } else if (!param.isPrimitiveType && param.dataType != null && !param.isArray && !param.isBodyParam && !param.isModel) { // Reference to a non-primitive type (e.g., enum via $ref) @@ -492,6 +502,37 @@ public Map postProcessAllModels(Map objs) } } + // Second pass: Make discriminator properties required in Zod schemas. + // Discriminator properties must not be .optional() so that the inferred + // type from z.discriminatedUnion matches the generated TypeScript types. + for (ModelsMap entry : result.values()) { + for (ModelMap mo : entry.getModels()) { + CodegenModel cm = mo.getModel(); + if (cm.vendorExtensions.containsKey("x-is-discriminated-union") && cm.discriminator != null) { + String discPropName = cm.discriminator.getPropertyBaseName(); + if (cm.oneOf != null) { + for (String memberName : cm.oneOf) { + ModelsMap memberModelsMap = result.get(memberName); + if (memberModelsMap != null) { + for (ModelMap mm : memberModelsMap.getModels()) { + for (CodegenProperty prop : mm.getModel().vars) { + if (prop.baseName.equals(discPropName) || prop.name.equals(discPropName)) { + String zodFull = (String) prop.vendorExtensions.get("x-zod-full"); + if (zodFull != null && zodFull.endsWith(".optional()")) { + prop.vendorExtensions.put("x-zod-full", + zodFull.substring(0, zodFull.length() - ".optional()".length())); + } + break; + } + } + } + } + } + } + } + } + } + return result; } diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache index 921b8027762c..9a189eb28215 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache @@ -39,7 +39,7 @@ export const {{classname}}Schema = z.object({ {{name}}: {{{vendorExtensions.x-zod-full}}}, {{/vendorExtensions.x-is-own-property}} {{/vars}} -}){{#vendorExtensions.x-intersection-parents}}.merge({{modelName}}Schema){{/vendorExtensions.x-intersection-parents}}; +}){{#vendorExtensions.x-intersection-parents}}.extend({{modelName}}Schema.shape){{/vendorExtensions.x-intersection-parents}}; {{/vendorExtensions.x-is-intersection}} {{^vendorExtensions.x-is-intersection}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache index 3facd91d43b7..3366022862f3 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache @@ -60,15 +60,17 @@ export const {{handlerName}} = const result = service.{{operationId}}({{#hasParams}}params{{/hasParams}}); if (Symbol.asyncIterator in Object(result)) { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }); + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("X-Accel-Buffering", "no"); + res.flushHeaders(); + res.socket?.setNoDelay(true); try { for await (const chunk of result as AsyncIterable) { const dto = mappers.{{{sseDtoMapper}}}(chunk); res.write(`id: ${dto.id}\nevent: ${dto.event}\ndata: ${JSON.stringify(dto.data)}\n\n`); + if (typeof (res as any).flush === "function") (res as any).flush(); } } catch (streamErr) { res.write(`event: error\ndata: ${JSON.stringify({ message: streamErr instanceof Error ? streamErr.message : "Stream error" })}\n\n`); @@ -115,7 +117,7 @@ export const {{handlerName}} = } catch (err) { if (err instanceof ZodError) { const statusCode = res.headersSent ? 500 : 400; - return next(errors.validationErrors(statusCode, err.errors)); + return next(errors.validationErrors(statusCode, err.issues)); } else { next(errors.unhandledException(err)); } diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache index ef5c4fd9c717..3782a9b8ed7f 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache @@ -39,7 +39,7 @@ export const {{classname}}Schema = z.object({ {{name}}: {{{vendorExtensions.x-zod-full}}}, {{/vendorExtensions.x-is-own-property}} {{/vars}} -}){{#vendorExtensions.x-intersection-parents}}.merge({{modelName}}Schema){{/vendorExtensions.x-intersection-parents}}; +}){{#vendorExtensions.x-intersection-parents}}.extend({{modelName}}Schema.shape){{/vendorExtensions.x-intersection-parents}}; {{/vendorExtensions.x-is-intersection}} {{^vendorExtensions.x-is-intersection}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache index 222529d22704..afed13d26434 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache @@ -30,7 +30,7 @@ export type {{operationIdPascal}}Params = { {{/pathParams}} {{#queryParams}} /** {{baseName}} */ - {{name}}{{^required}}?{{/required}}: {{dataType}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; {{/queryParams}} {{#hasBodyParam}} /** {{bodyParam.name}} */ From 21f4dccb04d4563db7e35399376124fc39095134 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Thu, 12 Feb 2026 20:55:12 -0500 Subject: [PATCH 05/15] Fix data type interpolation in TypeScript params and body parameters --- .../resources/typescript-express-zod-client/types.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache index 8b7862a084bf..545ea19ffd1e 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache @@ -30,11 +30,11 @@ export type {{operationIdPascal}}Params = { {{/pathParams}} {{#queryParams}} /** {{baseName}} */ - {{name}}{{^required}}?{{/required}}: {{dataType}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}; {{/queryParams}} {{#hasBodyParam}} /** {{bodyParam.name}} */ - {{bodyParam.name}}: {{bodyParam.dataType}}; + {{bodyParam.name}}: {{{bodyParam.dataType}}}; {{/hasBodyParam}} }; From c0b769e65e47e252e2fc2598540f820e393a9149 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Fri, 13 Feb 2026 08:42:47 -0500 Subject: [PATCH 06/15] Updated generator to generate express v5 code --- .../TypescriptExpressZodServerCodegen.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index 08039818e1ca..be8ad4962d81 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -84,6 +84,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L for (CodegenOperation op : ops) { // Convert {param} to :param for Express routing op.path = op.path.replaceAll("\\{([^}]+)\\}", ":$1"); + // Escape reserved path-to-regexp v8 characters for Express v5 compatibility + op.path = escapeExpressReservedChars(op.path); // Add vendor extensions for template use op.vendorExtensions.put("x-express-path", op.path); @@ -561,6 +563,38 @@ private String findDiscriminatorValue(String modelName, String discriminatorProp return null; } + /** + * Escape characters that are reserved in path-to-regexp v8 (used by Express v5). + * Reserved chars: ( ) [ ] ? + ! + * These are escaped with backslash in the generated TypeScript source. + * Double-backslash is used so that Mustache renders \\( in TS, which JS evaluates to \( at runtime. + * Parameter segments (:paramName) are left untouched. + * This is backward-compatible with Express v4 (path-to-regexp 0.1.x also treats \( as literal). + */ + private String escapeExpressReservedChars(String path) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < path.length()) { + char c = path.charAt(i); + if (c == ':') { + // Skip over :paramName segment + result.append(c); + i++; + while (i < path.length() && (Character.isLetterOrDigit(path.charAt(i)) || path.charAt(i) == '_')) { + result.append(path.charAt(i)); + i++; + } + } else if ("()[]?+!".indexOf(c) >= 0) { + result.append("\\\\").append(c); + i++; + } else { + result.append(c); + i++; + } + } + return result.toString(); + } + /** * Add Zod-specific vendor extensions to a model property. */ From 9625d8d0eb627409fbb830c3a4033ec5c4a0230f Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Sun, 15 Feb 2026 11:13:17 -0500 Subject: [PATCH 07/15] Enhance TypeScript Express Zod server and client error handling and type definitions --- .../TypescriptExpressZodClientCodegen.java | 45 +++++++++++++++++ .../TypescriptExpressZodServerCodegen.java | 2 +- .../http-client.mustache | 14 ++++-- .../express/handlers.mustache | 48 +++++++++---------- .../express/index.mustache | 2 + .../express/router-factory.mustache | 37 +++++++------- 6 files changed, 99 insertions(+), 49 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java index da1b9f4f86bf..4f3a6df1d524 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java @@ -678,6 +678,51 @@ public Map postProcessSupportingFileData(Map bun bundle.put("models", topologicallySortModels(models)); } + // Inspect ModelError schema and expose error-shape template variables + if (models != null) { + for (Object modelEntry : models) { + @SuppressWarnings("unchecked") + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm != null && "ModelError".equals(cm.classname)) { + // Determine the message field name: prefer "message", then "title", + // then first required string property, fallback to "message" + String messageField = null; + String firstRequiredString = null; + boolean hasCode = false; + boolean hasSource = false; + + for (CodegenProperty prop : cm.vars) { + if ("code".equals(prop.name)) { + hasCode = true; + } + if ("source".equals(prop.name)) { + hasSource = true; + } + if (prop.required && prop.isString) { + if ("message".equals(prop.name)) { + messageField = "message"; + } else if ("title".equals(prop.name) && messageField == null) { + messageField = "title"; + } + if (firstRequiredString == null) { + firstRequiredString = prop.name; + } + } + } + + if (messageField == null) { + messageField = firstRequiredString != null ? firstRequiredString : "message"; + } + + bundle.put("errorMessageField", messageField); + bundle.put("errorHasCode", hasCode); + bundle.put("errorHasSource", hasSource); + break; + } + } + } + return bundle; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index be8ad4962d81..a623b9439052 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -284,7 +284,7 @@ private void buildRequestHandlerType(CodegenOperation op) { if (op.bodyParam != null) { op.vendorExtensions.put("x-body-generic", "dtos." + op.bodyParam.dataType + "Dto"); } else { - op.vendorExtensions.put("x-body-generic", "never"); + op.vendorExtensions.put("x-body-generic", "unknown"); } // Response type diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache index cd6debf2fcdd..d2643c14dd4e 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache @@ -47,15 +47,19 @@ export interface ClientOptions { function defaultMapZodError(err: ZodError): types.ModelError[] { return err.issues.map((issue) => ({ - title: issue.message, - code: issue.code, + {{errorMessageField}}: issue.message, +{{#errorHasCode}} + code: String(issue.code), +{{/errorHasCode}} +{{#errorHasSource}} source: { pointer: issue.path.join("/") }, +{{/errorHasSource}} })); } function defaultMapUnhandledException(err: unknown): types.ModelError[] { const message = err instanceof Error ? err.message : String(err); - return [{ title: message }]; + return [{ {{errorMessageField}}: message }]; } {{#hasAnySSE}} @@ -168,9 +172,9 @@ export class Http{{name}} implements types.{{name}} { {{/responseType}} } catch (err) { if (err instanceof ZodError) { - return { errors: this.mapZodError(err) } as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + return { errors: this.mapZodError(err) } as unknown as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; } - return { errors: this.mapUnhandledException(err) } as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + return { errors: this.mapUnhandledException(err) } as unknown as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; } } diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache index 3366022862f3..133522c9d5b4 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache @@ -125,30 +125,30 @@ export const {{handlerName}} = }; {{/allOperations}} -function isError(result: { - errors: { status?: number | string }[]; - data?: any; -}): boolean { - return ( - !!result.errors.length && - (Array.isArray(result.data) - ? result.data.length === 0 - : result.data === undefined) - ); -} +type ErrorResult = { + errors: types.ModelError[]; + data?: unknown; +}; -function getHttpStatus( - success: number, - result: { errors: { status?: number | string }[]; data?: unknown }, -): number { - if (isError(result)) { - return result.errors.reduce((max, item) => { - if (typeof item.status === "undefined") return success; - const value = - typeof item.status === "string" ? Number(item.status) : item.status; - return !Number.isNaN(value) && value > max ? value : max; - }, success); - } else { - return success; +function isError(result: unknown): result is ErrorResult { + if (typeof result !== "object" || result === null || !("errors" in result)) { + return false; + } + const candidate = result as Record; + if (!Array.isArray(candidate.errors) || candidate.errors.length === 0) { + return false; } + return !("data" in result) || + candidate.data === undefined || + (Array.isArray(candidate.data) && candidate.data.length === 0); +} + +function getHttpStatus(success: number, result: unknown): number { + if (!isError(result)) return success; + return result.errors.reduce((max, item) => { + const status = (item as Record)["status"]; + if (status === undefined) return success; + const value = typeof status === "string" ? Number(status) : typeof status === "number" ? status : NaN; + return !Number.isNaN(value) && value > max ? value : max; + }, success); } diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache index cc69218c910d..057078aeffb6 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/index.mustache @@ -1,4 +1,6 @@ +{{#hasAuthOperations}} export * from "./auth-middleware"; +{{/hasAuthOperations}} export * from "./errors"; export * from "./handlers"; export * from "./router-factory"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache index 4dbaad21f079..04c3ecbc0d00 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache @@ -32,7 +32,7 @@ export function getRouter({ keyof expressTypes.Middleware, "_onlySwaggerUI" | "_exceptSwaggerUI" >, - >(methodName: TName, defaultHandler: RequestHandler): RequestHandler[] { + >(methodName: TName, defaultHandler: RequestHandler): RequestHandler[] { return [ ...getMiddleware(middleware, methodName), ...getHandlers(handlerOverrides?.[methodName], defaultHandler), @@ -106,27 +106,26 @@ export function getRouter({ * @param name - The name of the service method to get middleware for * @returns An array of {@link RequestHandler|express.RequestHandler} functions */ -function getMiddleware< - TName extends string, - TMiddleware extends Partial< - Record - >, ->( - middleware: TMiddleware | TMiddleware[] | undefined, - name: TName, -): RequestHandler[] { +function getMiddleware( + middleware: expressTypes.Middleware | expressTypes.Middleware[] | undefined, + name: keyof expressTypes.Middleware, +): RequestHandler[] { if (!middleware) return []; const middlewareArray = Array.isArray(middleware) ? middleware : [middleware]; return middlewareArray.flatMap((m) => { - const _exceptSwaggerUI = - m["_exceptSwaggerUI"] && name !== "_onlySwaggerUI" - ? m["_exceptSwaggerUI"] + const exceptSwaggerUI = + m._exceptSwaggerUI && name !== "_onlySwaggerUI" + ? m._exceptSwaggerUI : []; - const base = m[name] ? m[name] : []; + const handler = m[name]; + const base = handler ? handler : []; const baseArray = Array.isArray(base) ? base : [base]; - return [...baseArray, ..._exceptSwaggerUI]; + const exceptArray = Array.isArray(exceptSwaggerUI) + ? exceptSwaggerUI + : [exceptSwaggerUI]; + return [...baseArray, ...exceptArray]; }); } @@ -138,10 +137,10 @@ function getMiddleware< * @param defaultHandler - The default handler to use if no overrides are provided * @returns An array of {@link RequestHandler|express.RequestHandler} functions */ -function getHandlers( - overrides: TRequestHandler | TRequestHandler[] | undefined, - defaultHandler: TRequestHandler, -): TRequestHandler[] { +function getHandlers( + overrides: RequestHandler | RequestHandler[] | undefined, + defaultHandler: RequestHandler, +): RequestHandler[] { if (!overrides) return [defaultHandler]; return Array.isArray(overrides) ? overrides : [overrides]; } From fa7e59c0a9b2afafeb19773b72c27291b174e437 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Sun, 5 Jul 2026 13:17:58 -0400 Subject: [PATCH 08/15] [typescript-express-zod] Address PR review feedback on server/client generators Fixes for correctness/robustness issues raised in review of the typescript-express-zod-server and typescript-express-zod-client generators: - DTO mappers: guard referenced properties with `== null` (preserve null, never pass null into a mapper) for arrays and single refs, both directions. - Discriminated-union mappers: add a default case that throws instead of falling through to an undefined return. - Nullable handling: emit `T | null` in domain/DTO types and `.nullable()` in Zod schemas when a property is nullable. - Path parameters: use the schema-derived dataType instead of hardcoded string. - Request body: honor optionality (`?`) and use the correct (unescaped) type. - Models without properties: emit fallback Zod schema and DTO type alias. - Server handlers: validate the mapped wire payload (responseDto) against the response schema rather than the pre-mapped domain object. - SSE: client parser handles CRLF framing and multi-line data; server detects promise-wrapped async iterables. - Client: stop parsing JSON on void/no-content (e.g. 204) responses. - Swagger UI: escape `<` in the embedded schema to prevent script breakout. - Error type guards: guard against null/non-object before reading `.code`. - Router types: dedicated HandlerOverrides type without the Swagger-UI scopes. - Implement the previously failing server model unit test. Co-Authored-By: Claude Opus 4.8 --- .../TypescriptExpressZodClientCodegen.java | 7 ++- .../TypescriptExpressZodServerCodegen.java | 14 +++--- .../dtos/mappers.mustache | 22 +++++++-- .../dtos/types.mustache | 29 +++++++----- .../http-client.mustache | 40 +++++++++++----- .../schemas.mustache | 4 ++ .../types.mustache | 8 ++-- .../dtos/mappers.mustache | 22 +++++++-- .../dtos/types.mustache | 29 +++++++----- .../express/errors.mustache | 8 ++-- .../express/handlers.mustache | 21 +++++++-- .../express/router-factory.mustache | 2 +- .../express/types.mustache | 12 ++++- .../schemas.mustache | 4 ++ .../types.mustache | 8 ++-- ...criptExpressZodServerCodegenModelTest.java | 47 ++++++++++++++----- 16 files changed, 196 insertions(+), 81 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java index 4f3a6df1d524..503b282d4e08 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java @@ -449,8 +449,11 @@ private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { prop.vendorExtensions.put("x-zod-type", zodType); String fullZod = zodType; + if (prop.isNullable) { + fullZod = fullZod + ".nullable()"; + } if (!prop.required) { - fullZod = zodType + ".optional()"; + fullZod = fullZod + ".optional()"; } prop.vendorExtensions.put("x-zod-full", fullZod); } @@ -577,6 +580,7 @@ public Map postProcessSupportingFileData(Map bun pm.put("name", p.paramName); pm.put("baseName", p.baseName); pm.put("zodType", buildZodType(p)); + pm.put("dataType", p.dataType); pathParams.add(pm); } opInfo.put("pathParams", pathParams); @@ -601,6 +605,7 @@ public Map postProcessSupportingFileData(Map bun Map body = new HashMap<>(); body.put("name", op.vendorExtensions.get("x-body-param-name")); body.put("dataType", op.bodyParam.dataType); + body.put("required", op.bodyParam.required); opInfo.put("bodyParam", body); opInfo.put("hasBodyParam", true); } else { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index a623b9439052..25762aedf728 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -602,15 +602,13 @@ private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { String zodType = buildZodPropertyType(prop); prop.vendorExtensions.put("x-zod-type", zodType); - // Full expression including optional + // Full expression including nullability and optionality. String fullZod = zodType; + if (prop.isNullable) { + fullZod = fullZod + ".nullable()"; + } if (!prop.required) { - // Check if the zodType already ends with a schema reference - if (zodType.endsWith("Schema")) { - fullZod = zodType + ".optional()"; - } else { - fullZod = zodType + ".optional()"; - } + fullZod = fullZod + ".optional()"; } prop.vendorExtensions.put("x-zod-full", fullZod); } @@ -741,6 +739,7 @@ public Map postProcessSupportingFileData(Map bun pm.put("name", p.paramName); pm.put("baseName", p.baseName); pm.put("zodType", buildZodType(p)); + pm.put("dataType", p.dataType); pathParams.add(pm); } opInfo.put("pathParams", pathParams); @@ -765,6 +764,7 @@ public Map postProcessSupportingFileData(Map bun Map body = new HashMap<>(); body.put("name", op.vendorExtensions.get("x-body-param-name")); body.put("dataType", op.bodyParam.dataType); + body.put("required", op.bodyParam.required); body.put("mapper", op.vendorExtensions.get("x-body-mapper")); opInfo.put("bodyParam", body); opInfo.put("hasBodyParam", true); diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache index 7ad36428001a..7b8f3fe62cc4 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache @@ -28,6 +28,10 @@ export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{cla case "{{mappingName}}": return mapFrom{{modelName}}Dto(dto); {{/vendorExtensions.x-mapped-models}} + default: + throw new Error( + `Unknown {{classname}} discriminator: ${(dto as { {{{vendorExtensions.x-discriminator-property}}}?: unknown }).{{{vendorExtensions.x-discriminator-property}}}}`, + ); } } @@ -37,6 +41,10 @@ export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classnam case "{{mappingName}}": return mapTo{{modelName}}Dto(obj); {{/vendorExtensions.x-mapped-models}} + default: + throw new Error( + `Unknown {{classname}} discriminator: ${(obj as { {{{vendorExtensions.x-discriminator-property}}}?: unknown }).{{{vendorExtensions.x-discriminator-property}}}}`, + ); } } @@ -75,11 +83,14 @@ export function mapFrom{{classname}}Dto( {{#vars}} {{#vendorExtensions.x-is-ref}} {{#isArray}} - {{name}}: dto.{{name}}?.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), + {{name}}: + dto.{{name}} == null + ? dto.{{name}} + : dto.{{name}}.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), {{/isArray}} {{^isArray}} {{name}}: - typeof dto.{{name}} === "undefined" + dto.{{name}} == null ? dto.{{name}} : mapFrom{{vendorExtensions.x-ref-type}}Dto(dto.{{name}}), {{/isArray}} @@ -98,11 +109,14 @@ export function mapTo{{classname}}Dto( {{#vars}} {{#vendorExtensions.x-is-ref}} {{#isArray}} - {{name}}: obj.{{name}}?.map(mapTo{{vendorExtensions.x-ref-type}}Dto), + {{name}}: + obj.{{name}} == null + ? obj.{{name}} + : obj.{{name}}.map(mapTo{{vendorExtensions.x-ref-type}}Dto), {{/isArray}} {{^isArray}} {{name}}: - typeof obj.{{name}} === "undefined" + obj.{{name}} == null ? obj.{{name}} : mapTo{{vendorExtensions.x-ref-type}}Dto(obj.{{name}}), {{/isArray}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache index 51ecd3874d76..0d533fb7e2be 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache @@ -37,27 +37,27 @@ export type {{classname}}Dto = { {{^vendorExtensions.x-is-literal}} {{#vendorExtensions.x-is-enum-ref}} {{#isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{^isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} {{^vendorExtensions.x-is-enum-ref}} {{#isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{^isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} @@ -82,27 +82,27 @@ export type {{classname}}Dto = { {{^vendorExtensions.x-is-literal}} {{#vendorExtensions.x-is-enum-ref}} {{#isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{^isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} {{^vendorExtensions.x-is-enum-ref}} {{#isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{^isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} @@ -110,6 +110,11 @@ export type {{classname}}Dto = { {{/vars}} }; +{{/hasVars}} +{{^hasVars}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = types.{{classname}}; + {{/hasVars}} {{/vendorExtensions.x-is-intersection}} {{/vendorExtensions.x-is-union}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache index d2643c14dd4e..333cf90ab15c 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache @@ -76,24 +76,41 @@ async function* parseSSEStream( const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); + // Normalize CRLF / CR line endings to LF per the SSE spec. + buffer = buffer.replace(/\r\n|\r/g, "\n"); const parts = buffer.split("\n\n"); buffer = parts.pop() ?? ""; for (const part of parts) { if (!part.trim()) continue; - const fields: Record = {}; + let id = ""; + let event = ""; + const dataLines: string[] = []; for (const line of part.split("\n")) { + if (line.startsWith(":")) continue; // comment line const colonIdx = line.indexOf(":"); - if (colonIdx === -1) continue; - const key = line.slice(0, colonIdx).trim(); - const val = line.slice(colonIdx + 1).trim(); - fields[key] = val; + const field = colonIdx === -1 ? line : line.slice(0, colonIdx); + let val = colonIdx === -1 ? "" : line.slice(colonIdx + 1); + // A single leading space after the colon is part of the delimiter. + if (val.startsWith(" ")) val = val.slice(1); + switch (field) { + case "id": + id = val; + break; + case "event": + event = val; + break; + case "data": + dataLines.push(val); + break; + } } - if (!fields.data) continue; + if (dataLines.length === 0) continue; try { const envelope = { - id: fields.id ?? "", - event: fields.event ?? "", - data: JSON.parse(fields.data), + id, + event, + // Multiple data: lines are joined with newlines per the SSE spec. + data: JSON.parse(dataLines.join("\n")), }; yield mapper(schema.parse(envelope)); } catch { /* skip malformed events */ } @@ -152,7 +169,7 @@ export class Http{{name}} implements types.{{name}} { method: "{{fetchMethod}}", {{/hasFetchMethod}} headers: { "Content-Type": "application/json" }, - body: JSON.stringify(params.{{bodyParam.name}}), + body: JSON.stringify(params{{#allParamsOptional}}?{{/allParamsOptional}}.{{bodyParam.name}}), }); {{/hasBodyParam}} {{^hasBodyParam}} @@ -168,7 +185,8 @@ export class Http{{name}} implements types.{{name}} { return mappers.{{responseDtoMapper}}(schemas.{{responseSchema}}.parse(json)); {{/responseType}} {{^responseType}} - await response.json(); + // No response body expected (e.g. 204 No Content); nothing to parse. + return; {{/responseType}} } catch (err) { if (err instanceof ZodError) { diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache index 9a189eb28215..fc021fddb0d4 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache @@ -50,6 +50,10 @@ export const {{classname}}Schema = z.object({ {{/vars}} }); +{{/hasVars}} +{{^hasVars}} +export const {{classname}}Schema = z.object({}).catchall(z.unknown()); + {{/hasVars}} {{/vendorExtensions.x-is-intersection}} {{/vendorExtensions.x-is-union}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache index 545ea19ffd1e..17ef43a86fc9 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache @@ -26,7 +26,7 @@ export interface {{name}} { export type {{operationIdPascal}}Params = { {{#pathParams}} /** {{baseName}} */ - {{name}}: string; + {{name}}: {{{dataType}}}; {{/pathParams}} {{#queryParams}} /** {{baseName}} */ @@ -34,7 +34,7 @@ export type {{operationIdPascal}}Params = { {{/queryParams}} {{#hasBodyParam}} /** {{bodyParam.name}} */ - {{bodyParam.name}}: {{{bodyParam.dataType}}}; + {{bodyParam.name}}{{^bodyParam.required}}?{{/bodyParam.required}}: {{{bodyParam.dataType}}}; {{/hasBodyParam}} }; @@ -84,7 +84,7 @@ export type {{classname}} = { {{name}}: "{{{vendorExtensions.x-literal-value}}}"; {{/vendorExtensions.x-is-literal}} {{^vendorExtensions.x-is-literal}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-literal}} {{/vendorExtensions.x-is-own-property}} {{/vars}} @@ -108,7 +108,7 @@ export type {{classname}} = { {{name}}: "{{{vendorExtensions.x-literal-value}}}"; {{/vendorExtensions.x-is-literal}} {{^vendorExtensions.x-is-literal}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-literal}} {{/vars}} }; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache index 6d7b2e644782..1a8b3997c24a 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/mappers.mustache @@ -28,6 +28,10 @@ export function mapFrom{{classname}}Dto(dto: dtos.{{classname}}Dto): types.{{cla case "{{mappingName}}": return mapFrom{{modelName}}Dto(dto); {{/vendorExtensions.x-mapped-models}} + default: + throw new Error( + `Unknown {{classname}} discriminator: ${(dto as { {{{vendorExtensions.x-discriminator-property}}}?: unknown }).{{{vendorExtensions.x-discriminator-property}}}}`, + ); } } @@ -37,6 +41,10 @@ export function mapTo{{classname}}Dto(obj: types.{{classname}}): dtos.{{classnam case "{{mappingName}}": return mapTo{{modelName}}Dto(obj); {{/vendorExtensions.x-mapped-models}} + default: + throw new Error( + `Unknown {{classname}} discriminator: ${(obj as { {{{vendorExtensions.x-discriminator-property}}}?: unknown }).{{{vendorExtensions.x-discriminator-property}}}}`, + ); } } @@ -75,11 +83,14 @@ export function mapFrom{{classname}}Dto( {{#vars}} {{#vendorExtensions.x-is-ref}} {{#isArray}} - {{name}}: dto.{{name}}?.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), + {{name}}: + dto.{{name}} == null + ? dto.{{name}} + : dto.{{name}}.map(mapFrom{{vendorExtensions.x-ref-type}}Dto), {{/isArray}} {{^isArray}} {{name}}: - typeof dto.{{name}} === "undefined" + dto.{{name}} == null ? dto.{{name}} : mapFrom{{vendorExtensions.x-ref-type}}Dto(dto.{{name}}), {{/isArray}} @@ -98,11 +109,14 @@ export function mapTo{{classname}}Dto( {{#vars}} {{#vendorExtensions.x-is-ref}} {{#isArray}} - {{name}}: obj.{{name}}?.map(mapTo{{vendorExtensions.x-ref-type}}Dto), + {{name}}: + obj.{{name}} == null + ? obj.{{name}} + : obj.{{name}}.map(mapTo{{vendorExtensions.x-ref-type}}Dto), {{/isArray}} {{^isArray}} {{name}}: - typeof obj.{{name}} === "undefined" + obj.{{name}} == null ? obj.{{name}} : mapTo{{vendorExtensions.x-ref-type}}Dto(obj.{{name}}), {{/isArray}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache index 71d6708f0c03..9438e0969616 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/dtos/types.mustache @@ -37,27 +37,27 @@ export type {{classname}}Dto = { {{^vendorExtensions.x-is-literal}} {{#vendorExtensions.x-is-enum-ref}} {{#isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{^isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} {{^vendorExtensions.x-is-enum-ref}} {{#isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{^isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} @@ -82,27 +82,27 @@ export type {{classname}}Dto = { {{^vendorExtensions.x-is-literal}} {{#vendorExtensions.x-is-enum-ref}} {{#isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}[]{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{^isArray}} - {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}; + {{name}}{{^required}}?{{/required}}: types.{{{vendorExtensions.x-enum-type}}}{{#isNullable}} | null{{/isNullable}}; {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} {{^vendorExtensions.x-is-enum-ref}} {{#isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto[]{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{^isArray}} {{#vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto; + {{name}}{{^required}}?{{/required}}: {{{vendorExtensions.x-ref-type}}}Dto{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{^vendorExtensions.x-is-ref}} - {{name}}{{^required}}?{{/required}}: {{{dataType}}}; + {{name}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/vendorExtensions.x-is-ref}} {{/isArray}} {{/vendorExtensions.x-is-enum-ref}} @@ -110,6 +110,11 @@ export type {{classname}}Dto = { {{/vars}} }; +{{/hasVars}} +{{^hasVars}} +/** The over-the-wire representation of the {@link types.{{classname}}|{{classname}}} type. */ +export type {{classname}}Dto = types.{{classname}}; + {{/hasVars}} {{/vendorExtensions.x-is-intersection}} {{/vendorExtensions.x-is-union}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache index 8584bd075da2..b37504ff8a1a 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/errors.mustache @@ -11,7 +11,7 @@ export function methodNotAllowed(): MethodNotAllowedError { } export function isMethodNotAllowed(error: any): error is MethodNotAllowedError { - return error.code === "METHOD_NOT_ALLOWED"; + return typeof error === "object" && error !== null && error.code === "METHOD_NOT_ALLOWED"; } export type MethodNotAllowedError = { @@ -28,7 +28,7 @@ export function validationErrors( } export function isValidationErrors(error: any): error is ValidationErrorsError { - return error.code === "VALIDATION_ERRORS"; + return typeof error === "object" && error !== null && error.code === "VALIDATION_ERRORS"; } export type ValidationErrorsError = { @@ -45,7 +45,7 @@ export function handledException( } export function isHandledException(error: any): error is HandledExceptionError { - return error.code === "HANDLED_EXCEPTION"; + return typeof error === "object" && error !== null && error.code === "HANDLED_EXCEPTION"; } export type HandledExceptionError = { @@ -61,7 +61,7 @@ export function unhandledException(exception: any): UnhandledExceptionError { export function isUnhandledException( error: any, ): error is UnhandledExceptionError { - return error.code === "UNHANDLED_EXCEPTION"; + return typeof error === "object" && error !== null && error.code === "UNHANDLED_EXCEPTION"; } export type UnhandledExceptionError = { diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache index 133522c9d5b4..333f1016b041 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache @@ -57,7 +57,14 @@ export const {{handlerName}} = // Execute service method const service = getService(req, res); {{#hasSSE}} - const result = service.{{operationId}}({{#hasParams}}params{{/hasParams}}); + const invoked: unknown = service.{{operationId}}({{#hasParams}}params{{/hasParams}}); + // Support services that return an async iterable directly or a promise resolving to one. + const result: unknown = + invoked != null && + typeof (invoked as { then?: unknown }).then === "function" && + !(Symbol.asyncIterator in Object(invoked)) + ? await invoked + : invoked; if (Symbol.asyncIterator in Object(result)) { res.setHeader("Content-Type", "text/event-stream"); @@ -86,13 +93,17 @@ export const {{handlerName}} = {{#responseDtoMapper}} const responseDto = mappers.{{{responseDtoMapper}}}(resolved); res.status(status).json(responseDto); +{{#responseSchema}} + // Validate the wire payload actually sent to the client. + schemas.{{{responseSchema}}}.parse(responseDto); +{{/responseSchema}} {{/responseDtoMapper}} {{^responseDtoMapper}} res.status(status).json(resolved); -{{/responseDtoMapper}} {{#responseSchema}} schemas.{{{responseSchema}}}.parse(resolved); {{/responseSchema}} +{{/responseDtoMapper}} } } {{/hasSSE}} @@ -105,13 +116,17 @@ export const {{handlerName}} = {{#responseDtoMapper}} const responseDto = mappers.{{{responseDtoMapper}}}(result); res.status(status).json(responseDto); +{{#responseSchema}} + // Validate the wire payload actually sent to the client. + schemas.{{{responseSchema}}}.parse(responseDto); +{{/responseSchema}} {{/responseDtoMapper}} {{^responseDtoMapper}} res.status(status).json(result); -{{/responseDtoMapper}} {{#responseSchema}} schemas.{{{responseSchema}}}.parse(result); {{/responseSchema}} +{{/responseDtoMapper}} } {{/hasSSE}} } catch (err) { diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache index 04c3ecbc0d00..9b22c698d261 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/router-factory.mustache @@ -72,7 +72,7 @@ export function getRouter({
CONTENT NOT LOADED
+ + + +
CONTENT NOT LOADED
+ + + + `); + }) + .options((_, res) => { + res.set("Allow", "GET, HEAD, OPTIONS").sendStatus(204); + }) + .all(methodNotAllowed("GET, HEAD, OPTIONS")); + return router; +} +/** + * Gets a list of middleware functions for a given name from the supplied middleware object(s). + * If no middleware objects are supplied, an empty array is returned. + * Handlers are returned in the order they are defined in the middleware object(s). + * @param middleware - The middleware object(s) to extract middleware from + * @param name - The name of the service method to get middleware for + * @returns An array of {@link RequestHandler|express.RequestHandler} functions + */ +function getMiddleware( + middleware: expressTypes.Middleware | expressTypes.Middleware[] | undefined, + name: keyof expressTypes.Middleware, +): RequestHandler[] { + if (!middleware) return []; + const middlewareArray = Array.isArray(middleware) ? middleware : [middleware]; + + return middlewareArray.flatMap((m) => { + const exceptSwaggerUI = + m._exceptSwaggerUI && name !== "_onlySwaggerUI" + ? m._exceptSwaggerUI + : []; + + const handler = m[name]; + const base = handler ? handler : []; + const baseArray = Array.isArray(base) ? base : [base]; + const exceptArray = Array.isArray(exceptSwaggerUI) + ? exceptSwaggerUI + : [exceptSwaggerUI]; + return [...baseArray, ...exceptArray]; + }); +} + +/** + * Gets a list of handlers for a given service method. + * If overrides are provided, they are used in place of the default handler. + * If no overrides are provided, the default handler is used. + * @param overrides - The handler(s) to use in place of the default handler + * @param defaultHandler - The default handler to use if no overrides are provided + * @returns An array of {@link RequestHandler|express.RequestHandler} functions + */ +function getHandlers( + overrides: RequestHandler | RequestHandler[] | undefined, + defaultHandler: RequestHandler, +): RequestHandler[] { + if (!overrides) return [defaultHandler]; + return Array.isArray(overrides) ? overrides : [overrides]; +} diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/express/types.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/express/types.ts new file mode 100644 index 000000000000..7e2429771512 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/express/types.ts @@ -0,0 +1,117 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import type { Request, Response, RequestHandler } from "express"; + +import type * as types from "../types"; +import type * as dtos from "../dtos/types"; + +export type RouterFactoryInput = { + /** OpenAPI schema as a JSON object */ + schema: any; + + getAuthorsService: ( + req: Request, + res: Response, + ) => types.AuthorsService; + getCatalogService: ( + req: Request, + res: Response, + ) => types.CatalogService; + + middleware?: Middleware | Middleware[]; + handlerOverrides?: HandlerOverrides; + swaggerUiVersion?: string; +}; + +export type Middleware = { + /** Middleware to be applied to the Swagger UI handler. */ + _onlySwaggerUI?: RequestHandler | RequestHandler[]; + /** Middleware to be applied to all handlers _except_ Swagger UI after any other method-specific middleware. */ + _exceptSwaggerUI?: RequestHandler | RequestHandler[]; + createAuthor?: + | CreateAuthorRequestHandler + | CreateAuthorRequestHandler[]; + createBook?: + | CreateBookRequestHandler + | CreateBookRequestHandler[]; + deleteBook?: + | DeleteBookRequestHandler + | DeleteBookRequestHandler[]; + getAuthor?: + | GetAuthorRequestHandler + | GetAuthorRequestHandler[]; + getBook?: + | GetBookRequestHandler + | GetBookRequestHandler[]; + getLatestBookEvent?: + | GetLatestBookEventRequestHandler + | GetLatestBookEventRequestHandler[]; +}; + +/** + * Per-operation handler replacements. Unlike {@link Middleware}, the + * Swagger-UI scopes (`_onlySwaggerUI` / `_exceptSwaggerUI`) are not valid here + * because they are ignored when resolving handler overrides. + */ +export type HandlerOverrides = Omit< + Middleware, + "_onlySwaggerUI" | "_exceptSwaggerUI" +>; + +export type CreateAuthorRequestHandler = RequestHandler< + {}, + dtos.AuthorDto, + dtos.AuthorDto, + {} +>; + +export type CreateBookRequestHandler = RequestHandler< + {}, + dtos.BookDto, + dtos.CreateBookRequestDto, + {} +>; + +export type DeleteBookRequestHandler = RequestHandler< + { + bookId: string + }, + any, + unknown, + {} +>; + +export type GetAuthorRequestHandler = RequestHandler< + { + authorId: string + }, + dtos.AuthorDto, + unknown, + {} +>; + +export type GetBookRequestHandler = RequestHandler< + { + bookId: string + }, + dtos.BookDto, + unknown, + { + expand?: string + } +>; + +export type GetLatestBookEventRequestHandler = RequestHandler< + { + bookId: string + }, + dtos.BookEventDto, + unknown, + {} +>; + diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/index.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/index.ts new file mode 100644 index 000000000000..019e4c35bb62 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/index.ts @@ -0,0 +1,4 @@ +export * from "./types"; +export * from "./schemas"; +export * from "./dtos"; +export * from "./express"; diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts new file mode 100644 index 000000000000..f01413694de5 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts @@ -0,0 +1,75 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +import { z } from "zod"; + +export const AuthorSchema = z.object({ + id: z.coerce.number().int().optional(), + name: z.string(), +}); + +export const GenreSchema = z.enum(['FICTION', 'NONFICTION', 'POETRY']); + +export const BookSchema = z.object({ + id: z.coerce.number().int().optional(), + title: z.string(), + genre: GenreSchema.optional(), + author: AuthorSchema.nullable().optional(), + pageCount: z.coerce.number().int().nullable().optional(), +}); + +export const BookAddedEventSchema = z.object({ + type: z.literal("BOOK_ADDED"), + bookId: z.coerce.number().int(), +}); + +export const BookRemovedEventSchema = z.object({ + type: z.literal("BOOK_REMOVED"), + bookId: z.coerce.number().int(), +}); + +export const BookEventSchema = z.discriminatedUnion("type", [ + BookAddedEventSchema, + BookRemovedEventSchema, +]); + +export const CreateBookRequestSchema = z.object({ + title: z.string(), + genre: GenreSchema.optional(), + authorId: z.coerce.number().int().optional(), +}); + +export const ModelErrorSchema = z.object({ + message: z.string(), + code: z.string().optional(), +}); + +export const CreateAuthorParamsSchema = z.object({ + author: AuthorSchema, +}); + +export const CreateBookParamsSchema = z.object({ + createBookRequest: CreateBookRequestSchema, +}); + +export const DeleteBookParamsSchema = z.object({ + bookId: z.coerce.number().int(), +}); + +export const GetAuthorParamsSchema = z.object({ + authorId: z.coerce.number().int(), +}); + +export const GetBookParamsSchema = z.object({ + bookId: z.coerce.number().int(), + expand: z.boolean().optional(), +}); + +export const GetLatestBookEventParamsSchema = z.object({ + bookId: z.coerce.number().int(), +}); + diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/types.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/types.ts new file mode 100644 index 000000000000..1d4563b21708 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/types.ts @@ -0,0 +1,111 @@ +/** + * This code was generated by openapi-generator (typescript-express-zod-server). + * + * Changes to this file may cause incorrect behavior and will be lost if + * the code is regenerated. + */ + +/** Interface for the AuthorsService */ +export interface AuthorsService { + /** Create an author */ + createAuthor(params: CreateAuthorParams): Promise; + + /** Get an author by id */ + getAuthor(params: GetAuthorParams): Promise; + +} + +/** Interface for the CatalogService */ +export interface CatalogService { + /** Create a book */ + createBook(params: CreateBookParams): Promise; + + /** Delete a book */ + deleteBook(params: DeleteBookParams): Promise; + + /** Get a book by id */ + getBook(params: GetBookParams): Promise; + + /** Get the latest event for a book (discriminated union response) */ + getLatestBookEvent(params: GetLatestBookEventParams): Promise; + +} + +export type CreateAuthorParams = { + /** author */ + author: Author; +}; + +export type CreateBookParams = { + /** createBookRequest */ + createBookRequest: CreateBookRequest; +}; + +export type DeleteBookParams = { + /** bookId */ + bookId: number; +}; + +export type GetAuthorParams = { + /** authorId */ + authorId: number; +}; + +export type GetBookParams = { + /** bookId */ + bookId: number; + /** expand */ + expand?: boolean; +}; + +export type GetLatestBookEventParams = { + /** bookId */ + bookId: number; +}; + +export type Author = { + id?: number; + name: string; +}; + +export type Genre = 'FICTION' | 'NONFICTION' | 'POETRY'; + +export type Book = { + id?: number; + title: string; + genre?: Genre; + author?: Author | null; + pageCount?: number | null; +}; + +export type BookAddedEvent = { + type: "BOOK_ADDED"; + bookId: number; +}; + +export type BookRemovedEvent = { + type: "BOOK_REMOVED"; + bookId: number; +}; + +export type BookEvent = BookAddedEvent | BookRemovedEvent; + +export function isBookAddedEvent(obj: BookEvent): obj is BookAddedEvent { + return obj.type === "BOOK_ADDED"; +} + +export function isBookRemovedEvent(obj: BookEvent): obj is BookRemovedEvent { + return obj.type === "BOOK_REMOVED"; +} + +export type CreateBookRequest = { + title: string; + genre?: Genre; + authorId?: number; +}; + +export type ModelError = { + message: string; + code?: string; +}; + diff --git a/samples/server/petstore/typescript-express-zod-server/jest.config.js b/samples/server/petstore/typescript-express-zod-server/jest.config.js new file mode 100644 index 000000000000..ed8fc6ffeabf --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/test/**/*.e2e-spec.ts"], +}; diff --git a/samples/server/petstore/typescript-express-zod-server/package-lock.json b/samples/server/petstore/typescript-express-zod-server/package-lock.json new file mode 100644 index 000000000000..12ba8d55d303 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/package-lock.json @@ -0,0 +1,5010 @@ +{ + "name": "typescript-express-zod-server-petstore", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript-express-zod-server-petstore", + "version": "1.0.0", + "dependencies": { + "express": "^5.0.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.7.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz", + "integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-jest": { + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.0", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/samples/server/petstore/typescript-express-zod-server/package.json b/samples/server/petstore/typescript-express-zod-server/package.json new file mode 100644 index 000000000000..6abb72c0b3f7 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "typescript-express-zod-server-petstore", + "version": "1.0.0", + "private": true, + "description": "Sample project exercising the typescript-express-zod-server generated output", + "scripts": { + "build": "tsc --noEmit", + "test": "tsc --noEmit && jest" + }, + "dependencies": { + "express": "^5.0.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.7.3" + } +} diff --git a/samples/server/petstore/typescript-express-zod-server/src/app.ts b/samples/server/petstore/typescript-express-zod-server/src/app.ts new file mode 100644 index 000000000000..567b9400b4db --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/src/app.ts @@ -0,0 +1,24 @@ +/** + * Wires the generated router factory into an Express app using the hand-written + * service implementations. Exported as a factory so tests can create isolated + * app instances. + */ +import express from "express"; + +import { getRouter } from "../builds/default/express"; +import { authorsService, catalogService } from "./services"; + +export function createApp(): express.Express { + const app = express(); + app.use(express.json()); + + const router = getRouter({ + // Only consumed to serve Swagger UI at "/"; not needed for the API routes. + schema: {}, + getAuthorsService: () => authorsService, + getCatalogService: () => catalogService, + }); + + app.use(router); + return app; +} diff --git a/samples/server/petstore/typescript-express-zod-server/src/services.ts b/samples/server/petstore/typescript-express-zod-server/src/services.ts new file mode 100644 index 000000000000..c67a00a48415 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/src/services.ts @@ -0,0 +1,51 @@ +/** + * Hand-written implementations of the generated service interfaces. + * + * Each service is a plain object typed as the generated interface, so the + * method parameter/return types are contextually checked against the generated + * contract (in ../builds/default). The bodies use trivial in-memory data — the + * point of the sample is to exercise the generated routing / validation / + * DTO-mapping pipeline, not to be a real store. + */ +import type { + AuthorsService, + CatalogService, + Author, + Book, +} from "../builds/default/types"; + +const sampleAuthor: Author = { id: 1, name: "Ursula K. Le Guin" }; + +const sampleBook: Book = { + id: 1, + title: "A Wizard of Earthsea", + genre: "FICTION", + author: sampleAuthor, + pageCount: 183, +}; + +export const catalogService: CatalogService = { + async createBook(params) { + return { + ...sampleBook, + title: params.createBookRequest.title, + genre: params.createBookRequest.genre, + }; + }, + async deleteBook() {}, + async getBook(params) { + return { ...sampleBook, id: params.bookId }; + }, + async getLatestBookEvent(params) { + return { type: "BOOK_ADDED", bookId: params.bookId }; + }, +}; + +export const authorsService: AuthorsService = { + async createAuthor(params) { + return { ...params.author, id: params.author.id ?? 7 }; + }, + async getAuthor(params) { + return { ...sampleAuthor, id: params.authorId }; + }, +}; diff --git a/samples/server/petstore/typescript-express-zod-server/test/bookstore.e2e-spec.ts b/samples/server/petstore/typescript-express-zod-server/test/bookstore.e2e-spec.ts new file mode 100644 index 000000000000..d989bd7211e5 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/test/bookstore.e2e-spec.ts @@ -0,0 +1,51 @@ +import request from "supertest"; + +import { createApp } from "../src/app"; + +const app = createApp(); + +describe("typescript-express-zod-server (bookstore)", () => { + it("GET /books/{bookId} returns the book; the path param is coerced to a number", async () => { + const res = await request(app).get("/books/42"); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ id: 42, title: "A Wizard of Earthsea", genre: "FICTION" }); + // nullable $ref property is mapped through the DTO layer + expect(res.body.author).toMatchObject({ name: "Ursula K. Le Guin" }); + }); + + it("POST /books validates the request body and returns the created book", async () => { + const res = await request(app) + .post("/books") + .send({ title: "The Dispossessed", genre: "FICTION", authorId: 1 }); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ title: "The Dispossessed" }); + }); + + it("DELETE /books/{bookId} succeeds for a void operation", async () => { + const res = await request(app).delete("/books/1"); + expect(res.status).toBe(200); + }); + + it("GET /books/{bookId}/latest-event returns a discriminated-union member", async () => { + const res = await request(app).get("/books/5/latest-event"); + expect(res.status).toBe(200); + expect(res.body).toEqual({ type: "BOOK_ADDED", bookId: 5 }); + }); + + it("GET /authors/{authorId} returns the author", async () => { + const res = await request(app).get("/authors/9"); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ id: 9, name: "Ursula K. Le Guin" }); + }); + + it("rejects a path param that fails schema validation with a 4xx", async () => { + const res = await request(app).get("/books/not-a-number"); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it("returns 405 Method Not Allowed for an unsupported method on a known route", async () => { + const res = await request(app).patch("/books/1"); + expect(res.status).toBe(405); + }); +}); diff --git a/samples/server/petstore/typescript-express-zod-server/tsconfig.json b/samples/server/petstore/typescript-express-zod-server/tsconfig.json new file mode 100644 index 000000000000..678feb1055c0 --- /dev/null +++ b/samples/server/petstore/typescript-express-zod-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true, + "declaration": false, + "outDir": "dist" + }, + "include": ["builds/**/*.ts", "src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] +} From 8d783162d3dbc076576b34cecfdca73d878a0377 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Sun, 5 Jul 2026 22:06:55 -0400 Subject: [PATCH 13/15] feat(codegen): fix multi-level array shape resolution, body mapping guards, and SSE type rendering Addresses codegen discrepancies across the TypeScript-Express-Zod generator, server route templates, and client SDK output (Findings 1-4, 6-8, 10). TypescriptExpressZodSupport.java: - Fix Enum-$ref & free-form classification: Ensure leaves check `isEnum || isEnumRef`. Fall back to identity for free-form objects instead of generating fabricated Dto types/mappers (e.g., `dtos.StatusDto`). - Implement recursive shape resolution: Introduce depth-suffixed lambdas for `shapeOf`, `arrayShape`, and `mapShape`. Nested arrays (e.g., `Array>`) now structurally align across DTO arrays, Zod schemas, and runtime mappers. - Support nullable items: Properly emit `.nullable()` schemas alongside null-skipping element mappers. - Refactor `resolve()` signature to eliminate the unreachable 9-parameter variant, degrading to identity instead of fabricating an arbitrary DTO name when `rp == null`. Server Templates: - Guard body `mapFn` execution: Wrap body mappers in type-guards (e.g., checking `Array.isArray(b)`) to ensure malformed or absent bodies correctly fall through to Zod validation (400) instead of throwing runtime TypeErrors (500). - Fix SSE type casting: Render explicit `Promise<{{{responseDomainGeneric}}}>` wrappers for SSE branches returning JSON array 200 responses, cleaning up unused generic bindings. - Update `buildZodType` body fallback to emit standard Zod primitives (`z.string()`, `z.number()`, `z.boolean()`) for dereferenced-primitive bodies. Client SDK: - Handle optional bodies cleanly: Apply an undefined-check wrapper on optional body params to preserve fallback behavior for absent bodies instead of causing TS2345 compiler errors or runtime crashes. - Fix SSE client mapping: Ensure `*Stream` methods structurally map the outbound payload (e.g., `createRecipeStream` properly invokes its matching DTO mapper). - Align `buildZodType` primitive fallback behavior with the server implementation. Verification: - Added comprehensive integration test spec covering nested arrays, enum-refs, free-form objects, nullable items, and optional bodies. - All 6 sandbox outputs pass validation (`npm test` exit 0, `tsc` compilation success). Deferred: Non-model SSE event naming conventions, ResponseSchema collisions, query key escaping, and `buildZodType` duplication. --- .../TypescriptExpressZodClientCodegen.java | 148 ++++------ .../TypescriptExpressZodServerCodegen.java | 189 +++++------- .../TypescriptExpressZodSupport.java | 276 ++++++++++++++++++ .../http-client.mustache | 24 +- .../schemas.mustache | 6 + .../types.mustache | 20 +- .../express/handlers.mustache | 8 +- .../schemas.mustache | 8 +- .../types.mustache | 18 +- 9 files changed, 462 insertions(+), 235 deletions(-) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java index 45e723d33ca8..6c9fbbc64994 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java @@ -99,11 +99,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L op.vendorExtensions.put("x-fetch-method", httpMethodUpper); } - // Determine the response type and schema names - if (op.returnType != null) { - op.vendorExtensions.put("x-response-type", op.returnType); - op.vendorExtensions.put("x-response-schema", op.returnType + "Schema"); - } + // Response Zod schema / wire type / mapper are resolved (array/map/primitive + // aware) in postProcessSupportingFileData via TypescriptExpressZodSupport. // Build params type name String paramsType = camelize(op.operationId) + "Params"; @@ -193,10 +190,26 @@ private String buildZodType(CodegenParameter param) { if (param.minLength != null) { zod.append(".min(").append(param.minLength).append(")"); } - } else if (param.isArray) { - zod.append("z.array(z.string())"); // simplified + } else if (param.isArray && param.items != null) { + zod.append(TypescriptExpressZodSupport.zodExpr(param.items)).append(".array()"); + } else if (param.isMap && param.additionalProperties != null) { + zod.append("z.record(z.string(), ") + .append(TypescriptExpressZodSupport.zodExpr(param.additionalProperties)) + .append(")"); } else if (param.isModel || param.isBodyParam) { - zod.append(camelize(param.dataType) + "Schema"); + // Bodies whose schema dereferences to a bare primitive (e.g. a $ref'd enum + // handled as string by fromRequestBody) carry a primitive dataType with the + // primitive flags unset — camelize(dataType)+"Schema" would fabricate an + // unresolved symbol like StringSchema. + if ("string".equals(param.dataType)) { + zod.append("z.string()"); + } else if ("number".equals(param.dataType)) { + zod.append("z.number()"); + } else if ("boolean".equals(param.dataType)) { + zod.append("z.boolean()"); + } else { + zod.append(camelize(param.dataType) + "Schema"); + } } else { zod.append("z.string()"); if (param.maxLength != null) { @@ -459,79 +472,12 @@ private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { } /** - * Build a Zod type expression for a model property. + * Build a Zod type expression for a model property. Delegates to the shared + * {@link TypescriptExpressZodSupport#zodExpr} so the server and client generators + * cannot diverge on the recursive array/map/primitive logic. */ private String buildZodPropertyType(CodegenProperty prop) { - // Handle enum properties with allowable values - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - return "z.literal(\"" + values.get(0) + "\")"; - } - if (prop.complexType != null) { - return camelize(prop.complexType) + "Schema"; - } - if (values != null) { - StringBuilder sb = new StringBuilder("z.enum(["); - for (int i = 0; i < values.size(); i++) { - if (i > 0) sb.append(", "); - sb.append("\"").append(values.get(i)).append("\""); - } - sb.append("])"); - return sb.toString(); - } - } - - // Handle arrays (before complexType check) - if (prop.isArray && prop.items != null) { - String itemZod = buildZodPropertyType(prop.items); - return itemZod + ".array()"; - } - - // Handle $ref to another model or enum type - if (prop.complexType != null) { - return camelize(prop.complexType) + "Schema"; - } - - // Handle primitive types - if (prop.isInteger || prop.isLong) { - StringBuilder sb = new StringBuilder("z.coerce.number().int()"); - if (prop.minimum != null) { - sb.append(".gte(").append(prop.minimum).append(")"); - } - if (prop.maximum != null) { - sb.append(".lte(").append(prop.maximum).append(")"); - } - return sb.toString(); - } - - if (prop.isNumber || prop.isFloat || prop.isDouble) { - StringBuilder sb = new StringBuilder("z.number()"); - if (prop.minimum != null) { - sb.append(".gte(").append(prop.minimum).append(")"); - } - if (prop.maximum != null) { - sb.append(".lte(").append(prop.maximum).append(")"); - } - return sb.toString(); - } - - if (prop.isBoolean) { - return "z.boolean()"; - } - - if (prop.isString) { - StringBuilder sb = new StringBuilder("z.string()"); - if (prop.maxLength != null) { - sb.append(".max(").append(prop.maxLength).append(")"); - } - if (prop.minLength != null) { - sb.append(".min(").append(prop.minLength).append(")"); - } - return sb.toString(); - } - - return "z.any()"; + return TypescriptExpressZodSupport.zodExpr(prop); } @Override @@ -555,6 +501,17 @@ public Map postProcessSupportingFileData(Map bun Map opInfo = new HashMap<>(); String operationIdPascal = camelize(op.operationId); + // Resolve response/body shapes (array/map/primitive aware). The client converts + // wire json -> domain (FROM_WIRE) and domain -> wire before sending (TO_WIRE). + TypescriptExpressZodSupport.Resolved rr = op.returnType != null + ? TypescriptExpressZodSupport.resolveResponse( + op, TypescriptExpressZodSupport.Direction.FROM_WIRE) + : null; + TypescriptExpressZodSupport.Resolved rb = op.bodyParam != null + ? TypescriptExpressZodSupport.resolveBody( + op.bodyParam, TypescriptExpressZodSupport.Direction.TO_WIRE, + buildZodType(op.bodyParam)) + : null; opInfo.put("operationId", op.operationId); opInfo.put("operationIdPascal", operationIdPascal); opInfo.put("description", op.summary != null ? op.summary : op.notes); @@ -608,16 +565,34 @@ public Map postProcessSupportingFileData(Map bun body.put("required", op.bodyParam.required); opInfo.put("bodyParam", body); opInfo.put("hasBodyParam", true); + // Convert the domain body -> wire before JSON.stringify. Identity (x) => x + // for bare primitives so the template can always call it as a function. + // When the body can be undefined (optional requestBody, or params itself + // optional), guard the mapper so absent bodies keep the old + // JSON.stringify(undefined) -> no-body behavior instead of crashing. + String bodyMapFn = rb.mapFn != null ? rb.mapFn : "(x) => x"; + boolean bodyMayBeUndefined = !op.bodyParam.required + || Boolean.TRUE.equals(op.vendorExtensions.get("x-all-params-optional")); + if (rb.mapFn != null && bodyMayBeUndefined) { + bodyMapFn = "(b) => b === undefined ? undefined : (" + rb.mapFn + ")(b)"; + } + opInfo.put("bodyMapFn", bodyMapFn); } else { opInfo.put("hasBodyParam", false); } - // Response info + // Response info. The response Zod schema is emitted as a per-operation const + // in schemas.ts, and the parsed wire payload is converted to domain via mapFn + // (identity (x) => x for bare primitives, so it can always be called as a function). opInfo.put("responseType", op.returnType); - opInfo.put("responseSchema", op.vendorExtensions.get("x-response-schema")); - if (op.returnType != null) { - opInfo.put("responseDtoMapper", "mapFrom" + op.returnType + "Dto"); - opInfo.put("responseDtoType", op.returnType + "Dto"); + if (rr != null) { + opInfo.put("responseSchema", operationIdPascal + "ResponseSchema"); + opInfo.put("responseZodExpr", rr.zod); + opInfo.put("responseDtoMapper", rr.mapFn != null ? rr.mapFn : "(x) => x"); + opInfo.put("responseDtoGeneric", rr.dtoGeneric); + // Domain type qualified for http-client.ts (which imports types); e.g. + // types.Pet[], { [key: string]: number }, string. + opInfo.put("responseDomainGeneric", rr.domainGeneric); } // SSE detection @@ -640,11 +615,6 @@ public Map postProcessSupportingFileData(Map bun } opInfo.put("hasSSE", hasSSE); - // Body DTO mapper - if (op.bodyParam != null) { - opInfo.put("bodyDtoMapper", "mapTo" + op.bodyParam.dataType + "Dto"); - } - serviceMap.get(serviceName).add(opInfo); } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java index f560112c1138..2eabe6b3010d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java @@ -103,12 +103,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L op.vendorExtensions.put("x-service-getter", "get" + serviceName); } - // Determine the response type and schema names - if (op.returnType != null) { - op.vendorExtensions.put("x-response-type", op.returnType); - op.vendorExtensions.put("x-response-schema", op.returnType + "Schema"); - op.vendorExtensions.put("x-response-dto-mapper", "mapTo" + op.returnType + "Dto"); - } + // Response Zod schema / wire type / mapper are resolved (array/map/primitive + // aware) in postProcessSupportingFileData via TypescriptExpressZodSupport. // Mark whether this operation has only path params, only query params, etc. boolean hasOnlyPathParams = !op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; @@ -134,13 +130,9 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L } op.vendorExtensions.put("x-all-params-optional", allParamsOptional); - // For body params, determine the DTO mapper name + // The body Zod schema / wire type / mapper are resolved (array/map/primitive + // aware) in postProcessSupportingFileData; here we only fix the body param name. if (op.bodyParam != null) { - String bodyType = op.bodyParam.dataType; - String bodyMapperName = "mapFrom" + bodyType + "Dto"; - op.vendorExtensions.put("x-body-mapper", bodyMapperName); - op.vendorExtensions.put("x-body-type", bodyType); - // Get the request body name from x-codegen-request-body-name or default to "body" String bodyParamName = op.bodyParam.paramName; if (bodyParamName == null || bodyParamName.isEmpty()) { @@ -191,6 +183,11 @@ private String buildZodType(CodegenParameter param) { } sb.append("])"); zod.append(sb); + // Array-of-enum params (e.g. a repeated query param) — the param itself is + // flagged isEnum, so wrap the enum in .array() to match the Array type. + if (param.isArray) { + zod.append(".array()"); + } } else { zod.append(camelize(param.dataType) + "Schema"); } @@ -224,11 +221,27 @@ private String buildZodType(CodegenParameter param) { if (param.minLength != null) { zod.append(".min(").append(param.minLength).append(")"); } - } else if (param.isArray) { - zod.append("z.array(z.string())"); // simplified + } else if (param.isArray && param.items != null) { + zod.append(TypescriptExpressZodSupport.zodExpr(param.items)).append(".array()"); + } else if (param.isMap && param.additionalProperties != null) { + zod.append("z.record(z.string(), ") + .append(TypescriptExpressZodSupport.zodExpr(param.additionalProperties)) + .append(")"); } else if (param.isModel || param.isBodyParam) { - // Reference the model schema - zod.append(camelize(param.dataType) + "Schema"); + // Bodies whose schema dereferences to a bare primitive (e.g. a $ref'd enum + // handled as string by fromRequestBody) carry a primitive dataType with the + // primitive flags unset — camelize(dataType)+"Schema" would fabricate an + // unresolved symbol like StringSchema. + if ("string".equals(param.dataType)) { + zod.append("z.string()"); + } else if ("number".equals(param.dataType)) { + zod.append("z.number()"); + } else if ("boolean".equals(param.dataType)) { + zod.append("z.boolean()"); + } else { + // Reference the model schema + zod.append(camelize(param.dataType) + "Schema"); + } } else { zod.append("z.string()"); if (param.maxLength != null) { @@ -280,19 +293,8 @@ private void buildRequestHandlerType(CodegenOperation op) { } op.vendorExtensions.put("x-query-generic", queryType.toString()); - // Body type - if (op.bodyParam != null) { - op.vendorExtensions.put("x-body-generic", "dtos." + op.bodyParam.dataType + "Dto"); - } else { - op.vendorExtensions.put("x-body-generic", "unknown"); - } - - // Response type - if (op.returnType != null) { - op.vendorExtensions.put("x-response-generic", "dtos." + op.returnType + "Dto"); - } else { - op.vendorExtensions.put("x-response-generic", "any"); - } + // The body and response wire (DTO) generics for the RequestHandler type are resolved + // (array/map/primitive aware) in postProcessSupportingFileData; see resolveBody/resolveResponse. } @Override @@ -614,83 +616,12 @@ private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { } /** - * Build a Zod type expression for a model property. + * Build a Zod type expression for a model property. Delegates to the shared + * {@link TypescriptExpressZodSupport#zodExpr} so the server and client generators + * cannot diverge on the recursive array/map/primitive logic. */ private String buildZodPropertyType(CodegenProperty prop) { - // Handle enum properties with allowable values - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - // Single-value enum = literal - return "z.literal(\"" + values.get(0) + "\")"; - } - // Multi-value enum — reference the enum schema or inline it - if (prop.complexType != null) { - return camelize(prop.complexType) + "Schema"; - } - // Inline enum - if (values != null) { - StringBuilder sb = new StringBuilder("z.enum(["); - for (int i = 0; i < values.size(); i++) { - if (i > 0) sb.append(", "); - sb.append("\"").append(values.get(i)).append("\""); - } - sb.append("])"); - return sb.toString(); - } - } - - // Handle arrays (before complexType check to handle Array) - if (prop.isArray && prop.items != null) { - String itemZod = buildZodPropertyType(prop.items); - return itemZod + ".array()"; - } - - // Handle $ref to another model or enum type - if (prop.complexType != null) { - return camelize(prop.complexType) + "Schema"; - } - - // Handle primitive types - if (prop.isInteger || prop.isLong) { - StringBuilder sb = new StringBuilder("z.coerce.number().int()"); - if (prop.minimum != null) { - sb.append(".gte(").append(prop.minimum).append(")"); - } - if (prop.maximum != null) { - sb.append(".lte(").append(prop.maximum).append(")"); - } - return sb.toString(); - } - - if (prop.isNumber || prop.isFloat || prop.isDouble) { - StringBuilder sb = new StringBuilder("z.number()"); - if (prop.minimum != null) { - sb.append(".gte(").append(prop.minimum).append(")"); - } - if (prop.maximum != null) { - sb.append(".lte(").append(prop.maximum).append(")"); - } - return sb.toString(); - } - - if (prop.isBoolean) { - return "z.boolean()"; - } - - if (prop.isString) { - StringBuilder sb = new StringBuilder("z.string()"); - if (prop.maxLength != null) { - sb.append(".max(").append(prop.maxLength).append(")"); - } - if (prop.minLength != null) { - sb.append(".min(").append(prop.minLength).append(")"); - } - return sb.toString(); - } - - // Fallback - return "z.any()"; + return TypescriptExpressZodSupport.zodExpr(prop); } @Override @@ -718,6 +649,17 @@ public Map postProcessSupportingFileData(Map bun Map opInfo = new HashMap<>(); String operationIdPascal = camelize(op.operationId); + // Resolve response/body shapes (array/map/primitive aware). The server maps + // domain results -> wire (TO_WIRE) and req.body wire -> domain (FROM_WIRE). + TypescriptExpressZodSupport.Resolved rr = op.returnType != null + ? TypescriptExpressZodSupport.resolveResponse( + op, TypescriptExpressZodSupport.Direction.TO_WIRE) + : null; + TypescriptExpressZodSupport.Resolved rb = op.bodyParam != null + ? TypescriptExpressZodSupport.resolveBody( + op.bodyParam, TypescriptExpressZodSupport.Direction.FROM_WIRE, + buildZodType(op.bodyParam)) + : null; opInfo.put("operationId", op.operationId); opInfo.put("operationIdPascal", operationIdPascal); opInfo.put("description", op.summary != null ? op.summary : op.notes); @@ -765,7 +707,24 @@ public Map postProcessSupportingFileData(Map bun body.put("name", op.vendorExtensions.get("x-body-param-name")); body.put("dataType", op.bodyParam.dataType); body.put("required", op.bodyParam.required); - body.put("mapper", op.vendorExtensions.get("x-body-mapper")); + // Zod schema for the body field (array/map/primitive aware) and the + // wire->domain mapper function applied to req.body. mapFn is null for + // bare primitives (identity), in which case the template passes req.body through. + body.put("zodExpr", rb.zod); + // The mapper runs on RAW req.body before Zod validation, so guard by + // shape: a malformed body must fall through to the schema (ZodError -> + // 400) instead of crashing in the mapper (TypeError -> 500). + String bodyMapFn = rb.mapFn; + if (bodyMapFn != null) { + if (op.bodyParam.isArray) { + bodyMapFn = "(b) => Array.isArray(b) ? (" + rb.mapFn + ")(b) : b"; + } else if (op.bodyParam.isMap) { + bodyMapFn = "(b) => b !== null && typeof b === \"object\" && !Array.isArray(b) ? (" + rb.mapFn + ")(b) : b"; + } else { + bodyMapFn = "(b) => b == null ? b : (" + rb.mapFn + ")(b)"; + } + } + body.put("mapFn", bodyMapFn); opInfo.put("bodyParam", body); opInfo.put("hasBodyParam", true); } else { @@ -775,10 +734,18 @@ public Map postProcessSupportingFileData(Map bun // Auth permission from x-auth-permission vendor extension opInfo.put("permission", op.vendorExtensions.get("x-auth-permission")); - // Response info + // Response info. The response Zod schema is emitted as a per-operation + // const in schemas.ts (so array/map/primitive shapes have a real symbol to + // reference), and the payload is converted via mapFn (null => passthrough). opInfo.put("responseType", op.returnType); - opInfo.put("responseSchema", op.vendorExtensions.get("x-response-schema")); - opInfo.put("responseDtoMapper", op.vendorExtensions.get("x-response-dto-mapper")); + if (rr != null) { + opInfo.put("responseSchema", operationIdPascal + "ResponseSchema"); + opInfo.put("responseZodExpr", rr.zod); + opInfo.put("responseDtoMapper", rr.mapFn); + // Domain type qualified for use outside types.ts (e.g. the SSE branch's + // result cast in handlers.ts): types.Pet[], { [key: string]: number }, string. + opInfo.put("responseDomainGeneric", rr.domainGeneric); + } // SSE detection boolean hasSSE = false; @@ -802,8 +769,10 @@ public Map postProcessSupportingFileData(Map bun // RequestHandler type generics opInfo.put("paramsGeneric", op.vendorExtensions.get("x-params-generic")); opInfo.put("queryGeneric", op.vendorExtensions.get("x-query-generic")); - opInfo.put("bodyGeneric", op.vendorExtensions.get("x-body-generic")); - opInfo.put("responseGeneric", op.vendorExtensions.get("x-response-generic")); + // Wire (DTO) generics for the express RequestHandler type, resolved for + // array/map/primitive shapes (e.g. dtos.PetDto[], { [key: string]: number }). + opInfo.put("bodyGeneric", rb != null ? rb.dtoGeneric : "unknown"); + opInfo.put("responseGeneric", rr != null ? rr.dtoGeneric : "any"); serviceMap.get(serviceName).add(opInfo); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java new file mode 100644 index 000000000000..30580d8addc8 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java @@ -0,0 +1,276 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; + +import java.util.List; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +/** + * Shared helpers for the {@code typescript-express-zod-server} and + * {@code typescript-express-zod-client} generators. + * + *

The two generators are siblings (both extend {@code AbstractTypeScriptClientCodegen}, + * neither extends the other), so the recursive Zod builder and the array/map/primitive + * DTO resolution would otherwise be duplicated and free to diverge. Keeping that logic + * here — the highest-risk, must-stay-identical part — is the single source of truth. + * + *

The per-generator {@code postProcess*} methods keep their own vendor-extension wiring + * (which keys they set, {@code dtos.}/{@code mappers.} imports) but delegate the string + * computation to these static helpers. + */ +final class TypescriptExpressZodSupport { + private TypescriptExpressZodSupport() { + } + + /** Direction of a DTO mapping: domain->wire ({@code mapTo*Dto}) or wire->domain ({@code mapFrom*Dto}). */ + enum Direction { + TO_WIRE, FROM_WIRE + } + + /** Resolution of a body/response shape into its Zod schema, wire (DTO) type, and mapper. */ + static final class Resolved { + /** Zod schema expression, e.g. {@code PetSchema.array()}. */ + final String zod; + /** Over-the-wire TypeScript type, e.g. {@code dtos.PetDto[]}. */ + final String dtoGeneric; + /** Domain TypeScript type qualified for use outside {@code types.ts}, e.g. {@code types.Pet[]}. */ + final String domainGeneric; + /** Function expression converting between domain and wire, or {@code null} for identity/passthrough. */ + final String mapFn; + + Resolved(String zod, String dtoGeneric, String domainGeneric, String mapFn) { + this.zod = zod; + this.dtoGeneric = dtoGeneric; + this.domainGeneric = domainGeneric; + this.mapFn = mapFn; + } + } + + // --------------------------------------------------------------------- + // Recursive Zod expression builder + // --------------------------------------------------------------------- + + /** + * Build a Zod type expression for a model/response property. Handles enums, arrays, + * maps ({@code z.record}), $refs, and primitives (with numeric/length constraints). + */ + static String zodExpr(CodegenProperty prop) { + if (prop == null) { + return "z.any()"; + } + + // Enum properties with allowable values. + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + // Single-value enum = literal. + return "z.literal(\"" + values.get(0) + "\")"; + } + if (prop.complexType != null) { + // Multi-value enum referenced via $ref — reference the enum schema. + return camelize(prop.complexType) + "Schema"; + } + if (values != null) { + StringBuilder sb = new StringBuilder("z.enum(["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("\"").append(values.get(i)).append("\""); + } + sb.append("])"); + return sb.toString(); + } + } + + // Arrays (before the complexType check, to handle Array). + // Nullable items get `.nullable()` to match the TS type (`Array` + // from getTypeDeclaration); top-level nullability of model properties is + // applied externally via x-zod-full. + if (prop.isArray && prop.items != null) { + String item = zodExpr(prop.items); + if (prop.items.isNullable) item += ".nullable()"; + return item + ".array()"; + } + + // Maps / free-form objects with a typed value (additionalProperties). Zod v4 + // requires the two-arg z.record(keyType, valueType) signature. + if (prop.isMap && prop.additionalProperties != null) { + String value = zodExpr(prop.additionalProperties); + if (prop.additionalProperties.isNullable) value += ".nullable()"; + return "z.record(z.string(), " + value + ")"; + } + + // $ref to another model or enum type. + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + + // Primitive types. + if (prop.isInteger || prop.isLong) { + StringBuilder sb = new StringBuilder("z.coerce.number().int()"); + if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); + if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); + return sb.toString(); + } + if (prop.isNumber || prop.isFloat || prop.isDouble) { + StringBuilder sb = new StringBuilder("z.number()"); + if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); + if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); + return sb.toString(); + } + if (prop.isBoolean) { + return "z.boolean()"; + } + if (prop.isString) { + StringBuilder sb = new StringBuilder("z.string()"); + if (prop.maxLength != null) sb.append(".max(").append(prop.maxLength).append(")"); + if (prop.minLength != null) sb.append(".min(").append(prop.minLength).append(")"); + return sb.toString(); + } + + // Fallback. + return "z.any()"; + } + + // --------------------------------------------------------------------- + // Response / body shape resolution + // --------------------------------------------------------------------- + + /** + * Resolve an operation's response ({@code returnType}) into a Zod schema, wire type, + * and mapper. {@code dir} is {@code TO_WIRE} on the server (domain result -> wire) + * and {@code FROM_WIRE} on the client (wire json -> domain). + */ + static Resolved resolveResponse(CodegenOperation op, Direction dir) { + CodegenProperty rp = op.returnProperty; + String zod = zodExpr(rp); + if (rp == null) { + // DefaultCodegen assigns returnType and returnProperty together, so this is + // unreachable when callers guard on returnType != null; identity as a defensive + // fallback rather than fabricating a "Dto" symbol. + return new Resolved(zod, op.returnType, op.returnType, null); + } + Shape shape = shapeOf(rp, dir, 0); + return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); + } + + /** + * Resolve a request body parameter into a wire type and mapper. {@code zod} is the + * body's Zod expression (built by the caller's parameter builder). {@code dir} is + * {@code FROM_WIRE} on the server (req.body -> domain) and {@code TO_WIRE} on the + * client (domain -> wire before send). + */ + static Resolved resolveBody(CodegenParameter body, Direction dir, String zod) { + Shape shape; + if (body.isArray && body.items != null) { + shape = arrayShape(body.items, dir, 0); + } else if (body.isMap && body.additionalProperties != null) { + shape = mapShape(body.additionalProperties, dir, 0); + } else if (body.isModel) { + // For a scalar model body, CodegenParameter.dataType is the classname of the + // referenced model (e.g. "Pet"). Bodies that dereference to a primitive (e.g. + // a $ref'd enum handled as string by fromRequestBody) have isModel=false. + shape = new Shape("dtos." + body.dataType + "Dto", "types." + body.dataType, + "mappers." + mapPrefix(dir) + body.dataType + "Dto"); + } else { + // Bare primitive: domain == wire, identity mapping. + shape = new Shape(body.dataType, body.dataType, null); + } + return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); + } + + /** Wire type, domain type, and mapper (null => identity) for a resolved shape. */ + private static final class Shape { + final String dtoType; + final String domainType; + final String mapper; + + Shape(String dtoType, String domainType, String mapper) { + this.dtoType = dtoType; + this.domainType = domainType; + this.mapper = mapper; + } + } + + /** + * Recursively resolve a property's shape. Containers recurse into their element/value + * type (so nested shapes like {@code Array>} or map-of-array compose + * correctly); {@code depth} suffixes lambda parameters to avoid shadowing. + * + *

Note: container properties carry the innermost ref in {@code complexType} + * (DefaultCodegen.updatePropertyForArray/Map), so isArray/isMap MUST be checked + * before the complexType leaf branches. + */ + private static Shape shapeOf(CodegenProperty p, Direction dir, int depth) { + if (p.isArray && p.items != null) { + return arrayShape(p.items, dir, depth); + } + if (p.isMap && p.additionalProperties != null) { + return mapShape(p.additionalProperties, dir, depth); + } + if (p.complexType != null && !p.isEnum && !p.isEnumRef) { + // Object model — has a generated Dto + mappers. $ref'd enums set isEnumRef + // (not isEnum) and get neither, so they must not take this branch. + return new Shape("dtos." + p.complexType + "Dto", "types." + p.complexType, + "mappers." + mapPrefix(dir) + p.complexType + "Dto"); + } + if (p.complexType != null) { + // Enum (inline via isEnum or $ref via isEnumRef) — domain == wire, identity. + return new Shape("types." + p.complexType, "types." + p.complexType, null); + } + // Primitive or free-form object — domain == wire, identity. + return new Shape(p.dataType, p.dataType, null); + } + + private static Shape arrayShape(CodegenProperty items, Direction dir, int depth) { + Shape inner = shapeOf(items, dir, depth + 1); + String dtoElem = inner.dtoType; + String domainElem = inner.domainType; + String elemMapper = inner.mapper; + if (items.isNullable) { + // Match getTypeDeclaration's Array and skip nulls when mapping. + dtoElem = "(" + dtoElem + " | null)"; + domainElem = "(" + domainElem + " | null)"; + if (elemMapper != null) { + elemMapper = nullSafe(elemMapper, depth); + } + } + String mapper = elemMapper == null + ? null + : "(xs" + depth + ") => xs" + depth + ".map(" + elemMapper + ")"; + return new Shape(dtoElem + "[]", domainElem + "[]", mapper); + } + + private static Shape mapShape(CodegenProperty values, Direction dir, int depth) { + Shape inner = shapeOf(values, dir, depth + 1); + String dtoVal = inner.dtoType; + String domainVal = inner.domainType; + String valMapper = inner.mapper; + if (values.isNullable) { + dtoVal = dtoVal + " | null"; + domainVal = domainVal + " | null"; + if (valMapper != null) { + valMapper = nullSafe(valMapper, depth); + } + } + String mapper = valMapper == null + ? null + : "(m" + depth + ") => Object.fromEntries(Object.entries(m" + depth + + ").map(([k" + depth + ", v" + depth + "]) => [k" + depth + + ", (" + valMapper + ")(v" + depth + ")]))"; + return new Shape("{ [key: string]: " + dtoVal + " }", + "{ [key: string]: " + domainVal + " }", mapper); + } + + /** Wrap a mapper so null/undefined elements pass through untouched. */ + private static String nullSafe(String mapper, int depth) { + String e = "e" + depth; + return "(" + e + ") => " + e + " == null ? " + e + " : (" + mapper + ")(" + e + ")"; + } + + private static String mapPrefix(Direction dir) { + return dir == Direction.TO_WIRE ? "mapTo" : "mapFrom"; + } +} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache index c8584f870917..2c7176282dee 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache @@ -144,8 +144,8 @@ export class Http{{name}} implements types.{{name}} { } {{#operations}} - /** {{description}} */ - async {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}> { + /** {{{description}}} */ + async {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{{responseDomainGeneric}}}{{/responseType}}{{^responseType}}void{{/responseType}}> { {{#hasQueryParams}} const query = new URLSearchParams(); {{#queryParams}} @@ -164,25 +164,25 @@ export class Http{{name}} implements types.{{name}} { {{/hasQueryParams}} try { {{#hasBodyParam}} - const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url, { + const response = await this.fetch<{{#responseDtoGeneric}}{{{responseDtoGeneric}}}{{/responseDtoGeneric}}{{^responseDtoGeneric}}unknown{{/responseDtoGeneric}}>(url, { {{#hasFetchMethod}} method: "{{fetchMethod}}", {{/hasFetchMethod}} headers: { "Content-Type": "application/json" }, - body: JSON.stringify(params{{#allParamsOptional}}?{{/allParamsOptional}}.{{bodyParam.name}}), + body: JSON.stringify(({{{bodyMapFn}}})(params{{#allParamsOptional}}?{{/allParamsOptional}}.{{bodyParam.name}})), }); {{/hasBodyParam}} {{^hasBodyParam}} {{#hasFetchMethod}} - const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url, { method: "{{fetchMethod}}" }); + const response = await this.fetch<{{#responseDtoGeneric}}{{{responseDtoGeneric}}}{{/responseDtoGeneric}}{{^responseDtoGeneric}}unknown{{/responseDtoGeneric}}>(url, { method: "{{fetchMethod}}" }); {{/hasFetchMethod}} {{^hasFetchMethod}} - const response = await this.fetch<{{#responseDtoType}}dtos.{{responseDtoType}}{{/responseDtoType}}{{^responseDtoType}}unknown{{/responseDtoType}}>(url); + const response = await this.fetch<{{#responseDtoGeneric}}{{{responseDtoGeneric}}}{{/responseDtoGeneric}}{{^responseDtoGeneric}}unknown{{/responseDtoGeneric}}>(url); {{/hasFetchMethod}} {{/hasBodyParam}} {{#responseType}} const json = await response.json(); - return mappers.{{responseDtoMapper}}(schemas.{{responseSchema}}.parse(json)); + return ({{{responseDtoMapper}}})(schemas.{{responseSchema}}.parse(json)); {{/responseType}} {{^responseType}} // No response body expected (e.g. 204 No Content); nothing to parse. @@ -191,15 +191,15 @@ export class Http{{name}} implements types.{{name}} { {{/responseType}} } catch (err) { if (err instanceof ZodError) { - return { errors: this.mapZodError(err) } as unknown as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + return { errors: this.mapZodError(err) } as unknown as {{#responseType}}{{{responseDomainGeneric}}}{{/responseType}}{{^responseType}}any{{/responseType}}; } - return { errors: this.mapUnhandledException(err) } as unknown as {{#responseType}}types.{{responseType}}{{/responseType}}{{^responseType}}any{{/responseType}}; + return { errors: this.mapUnhandledException(err) } as unknown as {{#responseType}}{{{responseDomainGeneric}}}{{/responseType}}{{^responseType}}any{{/responseType}}; } } {{#hasSSE}} - /** {{description}} (streaming) */ - async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator { + /** {{{description}}} (streaming) */ + async *{{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: types.{{operationIdPascal}}Params{{/hasParams}}): AsyncGenerator { {{#hasQueryParams}} const query = new URLSearchParams(); {{#queryParams}} @@ -223,7 +223,7 @@ export class Http{{name}} implements types.{{name}} { {{/hasFetchMethod}} {{#hasBodyParam}} headers: { "Content-Type": "application/json", "Accept": "text/event-stream" }, - body: JSON.stringify(params.{{bodyParam.name}}), + body: JSON.stringify(({{{bodyMapFn}}})(params.{{bodyParam.name}})), {{/hasBodyParam}} {{^hasBodyParam}} headers: { "Accept": "text/event-stream" }, diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache index fc021fddb0d4..2ed4016f9fc7 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache @@ -61,3 +61,9 @@ export const {{classname}}Schema = z.object({}).catchall(z.unknown()); {{/isEnum}} {{/model}} {{/models}} +{{#allOperations}} +{{#responseType}} +export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}}; + +{{/responseType}} +{{/allOperations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache index 17ef43a86fc9..50cb4bc67d02 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache @@ -9,12 +9,12 @@ /** Interface for the {{name}} */ export interface {{name}} { {{#operations}} - /** {{description}} */ - {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}>; + /** {{{description}}} */ + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{{responseType}}}{{/responseType}}{{^responseType}}void{{/responseType}}>; {{#hasSSE}} - /** {{description}} (streaming) */ - {{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): AsyncIterable<{{sseType}}>; + /** {{{description}}} (streaming) */ + {{streamOperationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): AsyncIterable<{{{sseType}}}>; {{/hasSSE}} {{/operations}} @@ -49,7 +49,7 @@ export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last {{^isEnum}} {{#vendorExtensions.x-is-discriminated-union}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = {{#vendorExtensions.x-mapped-models}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; @@ -63,7 +63,7 @@ export function is{{modelName}}(obj: {{classname}}): obj is {{modelName}} { {{^vendorExtensions.x-is-discriminated-union}} {{#vendorExtensions.x-is-union}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; @@ -71,14 +71,14 @@ export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{ {{^vendorExtensions.x-is-union}} {{#vendorExtensions.x-is-intersection}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} {{#vendorExtensions.x-has-own-properties}} export type {{classname}} = { {{#vars}} {{#vendorExtensions.x-is-own-property}} {{#description}} - /** {{.}} */ + /** {{{.}}} */ {{/description}} {{#vendorExtensions.x-is-literal}} {{name}}: "{{{vendorExtensions.x-literal-value}}}"; @@ -97,12 +97,12 @@ export type {{classname}} = {{#vendorExtensions.x-intersection-parents}}{{modelN {{/vendorExtensions.x-is-intersection}} {{^vendorExtensions.x-is-intersection}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = { {{#vars}} {{#description}} - /** {{.}} */ + /** {{{.}}} */ {{/description}} {{#vendorExtensions.x-is-literal}} {{name}}: "{{{vendorExtensions.x-literal-value}}}"; diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache index 7e06d08a99e0..e744a98a0875 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/express/handlers.mustache @@ -48,7 +48,7 @@ export const {{handlerName}} = {{/hasDifferentName}} {{/queryParams}} {{#hasBodyParam}} - {{bodyParam.name}}: mappers.{{bodyParam.mapper}}(req.body), + {{bodyParam.name}}: {{#bodyParam.mapFn}}({{{bodyParam.mapFn}}})(req.body){{/bodyParam.mapFn}}{{^bodyParam.mapFn}}req.body{{/bodyParam.mapFn}}, {{/hasBodyParam}} }); @@ -85,7 +85,7 @@ export const {{handlerName}} = res.end(); } } else { - const resolved = await (result as Promise); + const resolved = await (result as Promise<{{{responseDomainGeneric}}}>); const status = getHttpStatus(200, resolved); if (isError(resolved)) { next(errors.handledException(status, resolved.errors)); @@ -94,7 +94,7 @@ export const {{handlerName}} = res, next, status, - {{#responseDtoMapper}}mappers.{{{responseDtoMapper}}}(resolved){{/responseDtoMapper}}{{^responseDtoMapper}}resolved{{/responseDtoMapper}}, + {{#responseDtoMapper}}({{{responseDtoMapper}}})(resolved){{/responseDtoMapper}}{{^responseDtoMapper}}resolved{{/responseDtoMapper}}, {{#responseSchema}} (payload) => schemas.{{{responseSchema}}}.parse(payload), {{/responseSchema}} @@ -112,7 +112,7 @@ export const {{handlerName}} = res, next, status, - {{#responseDtoMapper}}mappers.{{{responseDtoMapper}}}(result){{/responseDtoMapper}}{{^responseDtoMapper}}result{{/responseDtoMapper}}, + {{#responseDtoMapper}}({{{responseDtoMapper}}})(result){{/responseDtoMapper}}{{^responseDtoMapper}}result{{/responseDtoMapper}}, {{#responseSchema}} (payload) => schemas.{{{responseSchema}}}.parse(payload), {{/responseSchema}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache index 8b8d3fde49fa..5ff186b06921 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/schemas.mustache @@ -71,9 +71,15 @@ export const {{operationIdPascal}}ParamsSchema = z.object({ {{name}}: {{{zodType}}}, {{/queryParams}} {{#hasBodyParam}} - {{bodyParam.name}}: {{bodyParam.dataType}}Schema, + {{bodyParam.name}}: {{{bodyParam.zodExpr}}}, {{/hasBodyParam}} }); {{/hasParams}} {{/allOperations}} +{{#allOperations}} +{{#responseType}} +export const {{operationIdPascal}}ResponseSchema = {{{responseZodExpr}}}; + +{{/responseType}} +{{/allOperations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache index b168f592c687..ee9b46919b53 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-express-zod-server/types.mustache @@ -9,12 +9,12 @@ /** Interface for the {{name}} */ export interface {{name}} { {{#operations}} - /** {{description}} */ + /** {{{description}}} */ {{#hasSSE}} - {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}> | AsyncIterable<{{sseType}}>; + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{{responseType}}}{{/responseType}}{{^responseType}}void{{/responseType}}> | AsyncIterable<{{{sseType}}}>; {{/hasSSE}} {{^hasSSE}} - {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{responseType}}{{/responseType}}{{^responseType}}void{{/responseType}}>; + {{operationId}}({{#hasParams}}params{{#allParamsOptional}}?{{/allParamsOptional}}: {{operationIdPascal}}Params{{/hasParams}}): Promise<{{#responseType}}{{{responseType}}}{{/responseType}}{{^responseType}}void{{/responseType}}>; {{/hasSSE}} {{/operations}} @@ -49,7 +49,7 @@ export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last {{^isEnum}} {{#vendorExtensions.x-is-discriminated-union}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = {{#vendorExtensions.x-mapped-models}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-mapped-models}}; @@ -63,7 +63,7 @@ export function is{{modelName}}(obj: {{classname}}): obj is {{modelName}} { {{^vendorExtensions.x-is-discriminated-union}} {{#vendorExtensions.x-is-union}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-union-members}}; @@ -71,14 +71,14 @@ export type {{classname}} = {{#vendorExtensions.x-union-members}}{{modelName}}{{ {{^vendorExtensions.x-is-union}} {{#vendorExtensions.x-is-intersection}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} {{#vendorExtensions.x-has-own-properties}} export type {{classname}} = { {{#vars}} {{#vendorExtensions.x-is-own-property}} {{#description}} - /** {{.}} */ + /** {{{.}}} */ {{/description}} {{#vendorExtensions.x-is-literal}} {{name}}: "{{{vendorExtensions.x-literal-value}}}"; @@ -97,12 +97,12 @@ export type {{classname}} = {{#vendorExtensions.x-intersection-parents}}{{modelN {{/vendorExtensions.x-is-intersection}} {{^vendorExtensions.x-is-intersection}} {{#description}} -/** {{.}} */ +/** {{{.}}} */ {{/description}} export type {{classname}} = { {{#vars}} {{#description}} - /** {{.}} */ + /** {{{.}}} */ {{/description}} {{#vendorExtensions.x-is-literal}} {{name}}: "{{{vendorExtensions.x-literal-value}}}"; From 36dc3d99aaf337dffc1fb76305c71b49320be7b8 Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Sat, 11 Jul 2026 13:59:16 -0400 Subject: [PATCH 14/15] Migrate express-zod client to unified typescript generator; move spec to typescript-express-zod/petstore.yaml Excludes regenerated samples/ output (committed separately). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NpXMTYcZS92CKpMmtsjSTp --- .../typescript-express-zod-client.yaml | 5 +- .../typescript-express-zod-server.yaml | 2 +- docs/generators.md | 1 + .../typescript-express-zod-server.md | 290 +++++++ docs/generators/typescript.md | 2 +- .../languages/TypeScriptClientCodegen.java | 85 +- .../TypeScriptExpressZodClientUtils.java | 428 ++++++++++ ...=> TypeScriptExpressZodServerCodegen.java} | 375 +------- .../languages/TypeScriptExpressZodUtils.java | 645 ++++++++++++++ .../TypescriptExpressZodClientCodegen.java | 802 ------------------ .../TypescriptExpressZodSupport.java | 276 ------ .../org.openapitools.codegen.CodegenConfig | 4 +- .../api.mustache | 1 - .../model.mustache | 1 - .../express-zod}/dtos/index.mustache | 2 +- .../express-zod}/dtos/mappers.mustache | 2 +- .../express-zod}/dtos/types.mustache | 2 +- .../express-zod}/http-client.mustache | 2 +- .../express-zod}/index.mustache | 2 +- .../express-zod}/schemas.mustache | 2 +- .../express-zod}/types.mustache | 2 +- ...pressZodServerCodegenOptionsProvider.java} | 10 +- ...peScriptExpressZodClientFrameworkTest.java | 95 +++ ...riptExpressZodServerCodegenModelTest.java} | 6 +- ...ptExpressZodServerCodegenOptionsTest.java} | 12 +- ...TypeScriptExpressZodServerCodegenTest.java | 40 + ...TypescriptExpressZodServerCodegenTest.java | 19 - .../petstore.yaml} | 0 28 files changed, 1624 insertions(+), 1489 deletions(-) create mode 100644 docs/generators/typescript-express-zod-server.md create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java rename modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/{TypescriptExpressZodServerCodegen.java => TypeScriptExpressZodServerCodegen.java} (61%) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java delete mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java delete mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java delete mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache delete mode 100644 modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/dtos/index.mustache (65%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/dtos/mappers.mustache (98%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/dtos/types.mustache (98%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/http-client.mustache (99%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/index.mustache (71%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/schemas.mustache (95%) rename modules/openapi-generator/src/main/resources/{typescript-express-zod-client => typescript/express-zod}/types.mustache (97%) rename modules/openapi-generator/src/test/java/org/openapitools/codegen/options/{TypescriptExpressZodServerCodegenOptionsProvider.java => TypeScriptExpressZodServerCodegenOptionsProvider.java} (54%) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodClientFrameworkTest.java rename modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/{TypescriptExpressZodServerCodegenModelTest.java => TypeScriptExpressZodServerCodegenModelTest.java} (90%) rename modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/{TypescriptExpressZodServerCodegenOptionsTest.java => TypeScriptExpressZodServerCodegenOptionsTest.java} (58%) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenTest.java delete mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java rename modules/openapi-generator/src/test/resources/3_0/{typescript-express-zod.yaml => typescript-express-zod/petstore.yaml} (100%) diff --git a/bin/configs/typescript-express-zod-client.yaml b/bin/configs/typescript-express-zod-client.yaml index cde601360dd3..02f077a32395 100644 --- a/bin/configs/typescript-express-zod-client.yaml +++ b/bin/configs/typescript-express-zod-client.yaml @@ -1,5 +1,6 @@ -generatorName: typescript-express-zod-client +generatorName: typescript outputDir: samples/client/petstore/typescript-express-zod-client/builds/default -inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-express-zod.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-express-zod/petstore.yaml additionalProperties: + framework: express-zod hideGenerationTimestamp: "true" diff --git a/bin/configs/typescript-express-zod-server.yaml b/bin/configs/typescript-express-zod-server.yaml index e5d9794278e2..3b5a68e96543 100644 --- a/bin/configs/typescript-express-zod-server.yaml +++ b/bin/configs/typescript-express-zod-server.yaml @@ -1,5 +1,5 @@ generatorName: typescript-express-zod-server outputDir: samples/server/petstore/typescript-express-zod-server/builds/default -inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-express-zod.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-express-zod/petstore.yaml additionalProperties: hideGenerationTimestamp: "true" diff --git a/docs/generators.md b/docs/generators.md index ed22c4b53e16..d6e0898c5ac8 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -158,6 +158,7 @@ The following generators are available: * [scala-play-server](generators/scala-play-server.md) * [scalatra](generators/scalatra.md) * [spring](generators/spring.md) +* [typescript-express-zod-server (experimental)](generators/typescript-express-zod-server.md) * [typescript-nestjs-server (beta)](generators/typescript-nestjs-server.md) diff --git a/docs/generators/typescript-express-zod-server.md b/docs/generators/typescript-express-zod-server.md new file mode 100644 index 000000000000..02992fb126fc --- /dev/null +++ b/docs/generators/typescript-express-zod-server.md @@ -0,0 +1,290 @@ +--- +title: Documentation for the typescript-express-zod-server Generator +--- + +## METADATA + +| Property | Value | Notes | +| -------- | ----- | ----- | +| generator name | typescript-express-zod-server | pass this to the generate command after -g | +| generator stability | EXPERIMENTAL | | +| generator type | SERVER | | +| generator language | Typescript | | +| generator default templating engine | mustache | | +| helpTxt | Generates a TypeScript Express server with Zod validation. | | + +## CONFIG OPTIONS +These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details. + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|

**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| +|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|enumNameSuffix|Suffix that will be appended to all enum names.| |Enum| +|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |PascalCase| +|enumPropertyNamingReplaceSpecialChar|Set to true to replace '-' and '+' symbols with 'minus_' and 'plus_' in enum of type string| |false| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|
**false**
No changes to the enums are made, this is the default option.
**true**
With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false| +|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|licenseName|The name of the license| |Unlicense| +|modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name. Only change it if you provide your own run-time code for (de-)serialization of models| |original| +|npmName|The name under which you want to publish generated npm package. Required to generate a full package| |null| +|npmVersion|The version of your npm package. If not provided, using the version from the OpenAPI specification file.| |1.0.0| +|nullSafeAdditionalProps|Set to make additional properties types declare that their indexer may return undefined| |false| +|paramNaming|Naming convention for parameters: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase| +|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| +|snapshot|When setting this property to true, the version will be suffixed with -SNAPSHOT.yyyyMMddHHmm| |false| +|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| +|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| +|supportsES6|Generate code that conforms to ES6.| |false| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | +|array|Array| + + +## LANGUAGE PRIMITIVES + +
    +
  • Array
  • +
  • Awaited
  • +
  • Boolean
  • +
  • Capitalize
  • +
  • ConstructorParameters
  • +
  • Date
  • +
  • Double
  • +
  • Error
  • +
  • Exclude
  • +
  • Extract
  • +
  • File
  • +
  • Float
  • +
  • InstanceType
  • +
  • Integer
  • +
  • Long
  • +
  • Lowercase
  • +
  • Map
  • +
  • NoInfer
  • +
  • NonNullable
  • +
  • Object
  • +
  • Omit
  • +
  • OmitThisParameter
  • +
  • Parameters
  • +
  • Partial
  • +
  • Pick
  • +
  • Readonly
  • +
  • ReadonlyArray
  • +
  • Record
  • +
  • Required
  • +
  • ReturnType
  • +
  • Set
  • +
  • String
  • +
  • ThisParameterType
  • +
  • ThisType
  • +
  • Uncapitalize
  • +
  • Uppercase
  • +
  • any
  • +
  • boolean
  • +
  • number
  • +
  • object
  • +
  • string
  • +
+ +## RESERVED WORDS + +
    +
  • abstract
  • +
  • await
  • +
  • boolean
  • +
  • break
  • +
  • byte
  • +
  • case
  • +
  • catch
  • +
  • char
  • +
  • class
  • +
  • const
  • +
  • continue
  • +
  • debugger
  • +
  • default
  • +
  • delete
  • +
  • do
  • +
  • double
  • +
  • else
  • +
  • enum
  • +
  • export
  • +
  • extends
  • +
  • false
  • +
  • final
  • +
  • finally
  • +
  • float
  • +
  • for
  • +
  • formParams
  • +
  • function
  • +
  • goto
  • +
  • headerParams
  • +
  • if
  • +
  • implements
  • +
  • import
  • +
  • in
  • +
  • instanceof
  • +
  • int
  • +
  • interface
  • +
  • let
  • +
  • long
  • +
  • native
  • +
  • new
  • +
  • null
  • +
  • package
  • +
  • private
  • +
  • protected
  • +
  • public
  • +
  • queryParameters
  • +
  • requestOptions
  • +
  • return
  • +
  • short
  • +
  • static
  • +
  • super
  • +
  • switch
  • +
  • synchronized
  • +
  • this
  • +
  • throw
  • +
  • transient
  • +
  • true
  • +
  • try
  • +
  • typeof
  • +
  • useFormData
  • +
  • var
  • +
  • varLocalDeferred
  • +
  • varLocalPath
  • +
  • void
  • +
  • volatile
  • +
  • while
  • +
  • with
  • +
  • yield
  • +
+ +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✓|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✗|ToolingExtension +|MockServer|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Uuid|✗| +|Array|✓|OAS2,OAS3 +|Null|✗|OAS3 +|AnyType|✗|OAS2,OAS3 +|Object|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✓|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✓|OAS2,OAS3 +|Union|✗|OAS3 +|allOf|✗|OAS2,OAS3 +|anyOf|✗|OAS3 +|oneOf|✗|OAS3 +|not|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✗|OAS3 +|OAuth2_Implicit|✓|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 +|SignatureAuth|✗|OAS3 +|AWSV4Signature|✗|ToolingExtension + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✓|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✗|OAS2,OAS3 diff --git a/docs/generators/typescript.md b/docs/generators/typescript.md index 57474571765c..b6ba8b35cdab 100644 --- a/docs/generators/typescript.md +++ b/docs/generators/typescript.md @@ -27,7 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |enumType|Specify the enum type which should be used in the client code.|
**stringUnion**
Union of literal string types
**enum**
Typescript's [string enums](https://www.typescriptlang.org/docs/handbook/enums.html#string-enums)
|enum| |enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|
**false**
No changes to the enums are made, this is the default option.
**true**
With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false| |fileContentDataType|Specifies the type to use for the content of a file - i.e. Blob (Browser, Deno) / Buffer (node)| |Buffer| -|framework|Specify the framework which should be used in the client code.|
**fetch-api**
fetch-api
**jquery**
jquery
|fetch-api| +|framework|Specify the framework which should be used in the client code.|
**fetch-api**
fetch-api
**jquery**
jquery
**express-zod**
express-zod
|fetch-api| |importFileExtension|File extension to use with relative imports. Set it to '.js' or '.mjs' when using [ESM](https://nodejs.org/api/esm.html). Defaults to '.ts' when 'platform' is set to 'deno'.| |null| |legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| |licenseName|The name of the license| |Unlicense| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java index df0ed9077865..466dc177342b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java @@ -65,7 +65,8 @@ public class TypeScriptClientCodegen extends AbstractTypeScriptClientCodegen imp private static final String FRAMEWORK_SWITCH = "framework"; private static final String FRAMEWORK_SWITCH_DESC = "Specify the framework which should be used in the client code."; - private static final String[] FRAMEWORKS = {"fetch-api", "jquery"}; + private static final String[] FRAMEWORKS = {"fetch-api", "jquery", "express-zod"}; + private static final String EXPRESS_ZOD_FRAMEWORK = "express-zod"; private static final String PLATFORM_SWITCH = "platform"; private static final String PLATFORM_SWITCH_DESC = "Specifies the platform the code should run on. The default is 'node' for the 'request' framework and 'browser' otherwise."; private static final String[] PLATFORMS = {"browser", "node", "deno"}; @@ -219,11 +220,20 @@ public Map postProcessSupportingFileData(Map obj objs.put("fileContentDataType", additionalProperties.get(FILE_CONTENT_DATA_TYPE)); + if (isExpressZodFramework()) { + objs = TypeScriptExpressZodClientUtils.postProcessSupportingFileData(objs); + } + return objs; } @Override public OperationsMap postProcessOperationsWithModels(OperationsMap operations, List models) { + if (isExpressZodFramework()) { + // express-zod resolves its return types via TypeScriptExpressZodUtils; + // the 2xx-union returnType rewrite below would desync from that machinery. + return TypeScriptExpressZodClientUtils.postProcessOperations(operations); + } // Add additional filename information for model imports in the apis List> imports = operations.getImports(); @@ -384,6 +394,12 @@ private String getNameWithEnumPropertyNaming(String name) { @Override public ModelsMap postProcessModels(ModelsMap objs) { + if (isExpressZodFramework()) { + // express-zod templates render enums from enumVars and compute their own + // imports, so skip the enum namespacing / oneOf import filtering below. + return super.postProcessModels(objs); + } + // process enum in models List models = postProcessModelsEnum(objs).getModels(); for (ModelMap mo : models) { @@ -422,6 +438,15 @@ public ModelsMap postProcessModels(ModelsMap objs) { return objs; } + @Override + public Map postProcessAllModels(Map objs) { + Map result = super.postProcessAllModels(objs); + if (isExpressZodFramework()) { + TypeScriptExpressZodUtils.annotateModels(result, this.openAPI, this::toModelName); + } + return result; + } + @Override public String apiDocFileFolder() { return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); @@ -455,13 +480,19 @@ public String getHelp() { public void processOpts() { super.processOpts(); + additionalProperties.putIfAbsent(FRAMEWORK_SWITCH, FRAMEWORKS[0]); + + if (isExpressZodFramework()) { + configureExpressZodFramework(); + return; + } + // change package names apiPackage = this.apiPackage + ".apis"; testPackage = this.testPackage + ".tests"; additionalProperties.put("apiDocPath", apiDocPath); - additionalProperties.putIfAbsent(FRAMEWORK_SWITCH, FRAMEWORKS[0]); supportingFiles.add(new SupportingFile("index.mustache", "index.ts")); String httpLibName = this.getHttpLibForFramework(additionalProperties.get(FRAMEWORK_SWITCH).toString()); @@ -532,9 +563,59 @@ private String getHttpLibForFramework(String object) { return this.frameworkToHttpLibMap.get(object); } + private boolean isExpressZodFramework() { + return EXPRESS_ZOD_FRAMEWORK.equals(additionalProperties.get(FRAMEWORK_SWITCH)); + } + + /** + * Configure the {@code express-zod} framework: a typed HTTP client with Zod + * response validation matching the {@code typescript-express-zod-server} generator. + * Everything is emitted as supporting files from the {@code express-zod/} template + * subtree; the heavy post-processing lives in {@link TypeScriptExpressZodClientUtils} + * and {@link TypeScriptExpressZodUtils}. + */ + private void configureExpressZodFramework() { + // These options only affect the fetch-api/jquery template set. + for (String option : new String[]{PLATFORM_SWITCH, USE_RXJS_SWITCH, USE_INVERSIFY_SWITCH, + USE_OBJECT_PARAMS_SWITCH, ENUM_TYPE_SWITCH, IMPORT_FILE_EXTENSION_SWITCH, + FILE_CONTENT_DATA_TYPE}) { + if (additionalProperties.containsKey(option)) { + LOGGER.warn("Option '{}' is not supported by framework 'express-zod' and will be ignored.", option); + } + } + + // Restore the AbstractTypeScriptClientCodegen mappings that this class's + // constructor overrides: express-zod DTOs carry dates as strings over the wire + // and treat free-form objects as `object`. + typeMapping.put("object", "object"); + typeMapping.put("DateTime", "string"); + + apiPackage = ""; + modelPackage = ""; + + supportingFiles.clear(); + modelTemplateFiles.clear(); + apiTemplateFiles.clear(); + apiDocTemplateFiles.clear(); + + String dir = EXPRESS_ZOD_FRAMEWORK + File.separator; + supportingFiles.add(new SupportingFile(dir + "types.mustache", "", "types.ts")); + supportingFiles.add(new SupportingFile(dir + "schemas.mustache", "", "schemas.ts")); + supportingFiles.add(new SupportingFile(dir + "dtos" + File.separator + "types.mustache", "dtos", "types.ts")); + supportingFiles.add(new SupportingFile(dir + "dtos" + File.separator + "mappers.mustache", "dtos", "mappers.ts")); + supportingFiles.add(new SupportingFile(dir + "dtos" + File.separator + "index.mustache", "dtos", "index.ts")); + supportingFiles.add(new SupportingFile(dir + "http-client.mustache", "", "http-client.ts")); + supportingFiles.add(new SupportingFile(dir + "index.mustache", "", "index.ts")); + } + @Override public String getTypeDeclaration(Schema p) { + if (isExpressZodFramework()) { + // express-zod emits no HttpFile symbol and keeps the abstract base's + // type declarations (parity with the typescript-express-zod-server generator). + return super.getTypeDeclaration(p); + } if (ModelUtils.isMapSchema(p)) { Schema inner = getSchemaAdditionalProperties(p); String postfix = ""; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java new file mode 100644 index 000000000000..b583a2dfd87b --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodClientUtils.java @@ -0,0 +1,428 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.CodegenMediaType; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.CodegenResponse; +import org.openapitools.codegen.model.ApiInfoMap; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +/** + * Client-side post-processing for the {@code express-zod} framework of the unified + * {@code typescript} generator. Holds the logic that used to live in the standalone + * {@code typescript-express-zod-client} generator so {@code TypeScriptClientCodegen} + * only needs thin, framework-guarded delegation calls. + * + *

Logic shared with the {@code typescript-express-zod-server} generator (Zod + * expression building, model annotation, shape resolution, topological sorting) + * lives in {@link TypeScriptExpressZodUtils}. + */ +final class TypeScriptExpressZodClientUtils { + private TypeScriptExpressZodClientUtils() { + } + + /** + * Operation post-processing: URL templates, service names, fetch methods, params + * types, and per-parameter Zod vendor extensions. + */ + static OperationsMap postProcessOperations(OperationsMap operations) { + OperationMap objs = operations.getOperations(); + List ops = objs.getOperation(); + + for (CodegenOperation op : ops) { + // Build URL template for JS template literals: + // /recipe/{recipeId} -> /recipe/${encodeURIComponent(params.recipeId)} + String urlTemplate = op.path.replaceAll( + "\\{([^}]+)\\}", + "\\${encodeURIComponent(String(params.$1))}" + ); + op.vendorExtensions.put("x-url-template", urlTemplate); + + // Determine the service name from the tag + if (op.tags != null && !op.tags.isEmpty()) { + String tag = op.tags.get(0).getName(); + String serviceName = camelize(tag) + "Service"; + op.vendorExtensions.put("x-service-name", serviceName); + } + + // Set fetch method only for non-GET (GET is fetch default) + String httpMethodUpper = op.httpMethod.toUpperCase(Locale.ENGLISH); + if (!"GET".equals(httpMethodUpper)) { + op.vendorExtensions.put("x-fetch-method", httpMethodUpper); + } + + // Response Zod schema / wire type / mapper are resolved (array/map/primitive + // aware) in postProcessSupportingFileData via TypeScriptExpressZodUtils. + + // Build params type name + String paramsType = camelize(op.operationId) + "Params"; + op.vendorExtensions.put("x-params-type", paramsType); + + // Check if all params are optional + boolean allParamsOptional = true; + for (CodegenParameter param : op.allParams) { + if (param.required) { + allParamsOptional = false; + break; + } + } + op.vendorExtensions.put("x-all-params-optional", allParamsOptional); + + // Body param info + boolean hasBodyParam = op.bodyParam != null; + op.vendorExtensions.put("x-has-body-param", hasBodyParam); + if (hasBodyParam) { + String bodyParamName = op.bodyParam.paramName; + if (bodyParamName == null || bodyParamName.isEmpty()) { + bodyParamName = "body"; + } + op.vendorExtensions.put("x-body-param-name", bodyParamName); + } + + boolean hasNoParams = op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; + op.vendorExtensions.put("x-has-no-params", hasNoParams); + + // Add vendor extensions to each parameter for Zod schema building + for (CodegenParameter param : op.allParams) { + addZodVendorExtensions(param); + } + } + + return operations; + } + + /** + * Add Zod-specific vendor extensions to a parameter. + */ + private static void addZodVendorExtensions(CodegenParameter param) { + String zodType = buildZodType(param); + param.vendorExtensions.put("x-zod-type", zodType); + + // Track if the query param name differs from the camelCase name + if (param.isQueryParam && !param.baseName.equals(param.paramName)) { + param.vendorExtensions.put("x-original-name", param.baseName); + param.vendorExtensions.put("x-has-different-name", true); + } + } + + /** + * Build a Zod type expression for a parameter. This is the client variant; the + * server's {@code buildZodType} differs (inline z.enum branch) and unifying the + * two would change generated output — deferred as a follow-up. + */ + static String buildZodType(CodegenParameter param) { + StringBuilder zod = new StringBuilder(); + + if (param.isEnum) { + zod.append(camelize(param.dataType) + "Schema"); + } else if (!param.isPrimitiveType && param.dataType != null && !param.isArray + && !param.isBodyParam && !param.isModel) { + zod.append(camelize(param.dataType) + "Schema"); + } else if (param.isInteger || param.isLong) { + zod.append("z.coerce.number().int()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isNumber || param.isFloat || param.isDouble) { + zod.append("z.number()"); + if (param.minimum != null) { + zod.append(".gte(").append(param.minimum).append(")"); + } + if (param.maximum != null) { + zod.append(".lte(").append(param.maximum).append(")"); + } + } else if (param.isBoolean) { + zod.append("z.boolean()"); + } else if (param.isString) { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + if (param.minLength != null) { + zod.append(".min(").append(param.minLength).append(")"); + } + } else if (param.isArray && param.items != null) { + zod.append(TypeScriptExpressZodUtils.zodExpr(param.items)).append(".array()"); + } else if (param.isMap && param.additionalProperties != null) { + zod.append("z.record(z.string(), ") + .append(TypeScriptExpressZodUtils.zodExpr(param.additionalProperties)) + .append(")"); + } else if (param.isModel || param.isBodyParam) { + // Bodies whose schema dereferences to a bare primitive (e.g. a $ref'd enum + // handled as string by fromRequestBody) carry a primitive dataType with the + // primitive flags unset — camelize(dataType)+"Schema" would fabricate an + // unresolved symbol like StringSchema. + if ("string".equals(param.dataType)) { + zod.append("z.string()"); + } else if ("number".equals(param.dataType)) { + zod.append("z.number()"); + } else if ("boolean".equals(param.dataType)) { + zod.append("z.boolean()"); + } else { + zod.append(camelize(param.dataType) + "Schema"); + } + } else { + zod.append("z.string()"); + if (param.maxLength != null) { + zod.append(".max(").append(param.maxLength).append(")"); + } + } + + if (!param.required) { + zod.append(".optional()"); + } + + return zod.toString(); + } + + /** + * Bundle builder for the supporting-file templates: services, flat operation list, + * resolved response/body shapes, SSE stream operations, ModelError inspection, and + * topologically sorted models. + */ + @SuppressWarnings("unchecked") + static Map postProcessSupportingFileData(Map bundle) { + List allOps = ((ApiInfoMap) bundle.get("apiInfo")).getApis(); + + // Collect all unique services + Map>> serviceMap = new LinkedHashMap<>(); + + for (OperationsMap opsMap : allOps) { + OperationMap operationMap = opsMap.getOperations(); + List ops = operationMap.getOperation(); + + for (CodegenOperation op : ops) { + String serviceName = (String) op.vendorExtensions.getOrDefault("x-service-name", "DefaultService"); + + serviceMap.computeIfAbsent(serviceName, k -> new ArrayList<>()); + + Map opInfo = new HashMap<>(); + String operationIdPascal = camelize(op.operationId); + // Resolve response/body shapes (array/map/primitive aware). The client converts + // wire json -> domain (FROM_WIRE) and domain -> wire before sending (TO_WIRE). + TypeScriptExpressZodUtils.Resolved rr = op.returnType != null + ? TypeScriptExpressZodUtils.resolveResponse( + op, TypeScriptExpressZodUtils.Direction.FROM_WIRE) + : null; + TypeScriptExpressZodUtils.Resolved rb = op.bodyParam != null + ? TypeScriptExpressZodUtils.resolveBody( + op.bodyParam, TypeScriptExpressZodUtils.Direction.TO_WIRE, + buildZodType(op.bodyParam)) + : null; + opInfo.put("operationId", op.operationId); + opInfo.put("operationIdPascal", operationIdPascal); + opInfo.put("description", op.summary != null ? op.summary : op.notes); + opInfo.put("httpMethod", op.httpMethod.toLowerCase(Locale.ENGLISH)); + opInfo.put("httpMethodUpper", op.httpMethod.toUpperCase(Locale.ENGLISH)); + opInfo.put("serviceName", serviceName); + opInfo.put("returnType", op.returnType); + opInfo.put("hasParams", !op.allParams.isEmpty()); + opInfo.put("hasNoParams", op.allParams.isEmpty()); + opInfo.put("allParamsOptional", op.vendorExtensions.get("x-all-params-optional")); + + // URL template for JS template literals + opInfo.put("urlTemplate", op.vendorExtensions.get("x-url-template")); + + // Fetch method (only for non-GET) + opInfo.put("fetchMethod", op.vendorExtensions.get("x-fetch-method")); + opInfo.put("hasFetchMethod", op.vendorExtensions.containsKey("x-fetch-method")); + + // Params info + List> pathParams = new ArrayList<>(); + for (CodegenParameter p : op.pathParams) { + Map pm = new HashMap<>(); + pm.put("name", p.paramName); + pm.put("baseName", p.baseName); + pm.put("zodType", buildZodType(p)); + pm.put("dataType", p.dataType); + pathParams.add(pm); + } + opInfo.put("pathParams", pathParams); + opInfo.put("hasPathParams", !pathParams.isEmpty()); + + List> queryParams = new ArrayList<>(); + for (CodegenParameter p : op.queryParams) { + Map qm = new HashMap<>(); + qm.put("name", p.paramName); + qm.put("baseName", p.baseName); + qm.put("hasDifferentName", !p.baseName.equals(p.paramName)); + qm.put("zodType", buildZodType(p)); + qm.put("dataType", p.dataType); + qm.put("required", p.required); + qm.put("isEnum", p.isEnum); + queryParams.add(qm); + } + opInfo.put("queryParams", queryParams); + opInfo.put("hasQueryParams", !queryParams.isEmpty()); + + if (op.bodyParam != null) { + Map body = new HashMap<>(); + body.put("name", op.vendorExtensions.get("x-body-param-name")); + body.put("dataType", op.bodyParam.dataType); + body.put("required", op.bodyParam.required); + opInfo.put("bodyParam", body); + opInfo.put("hasBodyParam", true); + // Convert the domain body -> wire before JSON.stringify. Identity (x) => x + // for bare primitives so the template can always call it as a function. + // When the body can be undefined (optional requestBody, or params itself + // optional), guard the mapper so absent bodies keep the old + // JSON.stringify(undefined) -> no-body behavior instead of crashing. + String bodyMapFn = rb.mapFn != null ? rb.mapFn : "(x) => x"; + boolean bodyMayBeUndefined = !op.bodyParam.required + || Boolean.TRUE.equals(op.vendorExtensions.get("x-all-params-optional")); + if (rb.mapFn != null && bodyMayBeUndefined) { + bodyMapFn = "(b) => b === undefined ? undefined : (" + rb.mapFn + ")(b)"; + } + opInfo.put("bodyMapFn", bodyMapFn); + } else { + opInfo.put("hasBodyParam", false); + } + + // Response info. The response Zod schema is emitted as a per-operation const + // in schemas.ts, and the parsed wire payload is converted to domain via mapFn + // (identity (x) => x for bare primitives, so it can always be called as a function). + opInfo.put("responseType", op.returnType); + if (rr != null) { + opInfo.put("responseSchema", operationIdPascal + "ResponseSchema"); + opInfo.put("responseZodExpr", rr.zod); + opInfo.put("responseDtoMapper", rr.mapFn != null ? rr.mapFn : "(x) => x"); + opInfo.put("responseDtoGeneric", rr.dtoGeneric); + // Domain type qualified for http-client.ts (which imports types); e.g. + // types.Pet[], { [key: string]: number }, string. + opInfo.put("responseDomainGeneric", rr.domainGeneric); + } + + // SSE detection + boolean hasSSE = false; + for (CodegenResponse response : op.responses) { + if (response.is2xx && response.getContent() != null) { + CodegenMediaType sseMt = response.getContent().get("text/event-stream"); + if (sseMt != null && sseMt.getSchema() != null) { + hasSSE = true; + String sseType = sseMt.getSchema().getComplexType(); + if (sseType == null) sseType = sseMt.getSchema().getDataType(); + opInfo.put("sseType", sseType); + opInfo.put("sseDtoType", sseType + "Dto"); + opInfo.put("sseDtoMapper", "mapFrom" + sseType + "Dto"); + opInfo.put("sseSchema", sseType + "Schema"); + opInfo.put("streamOperationId", op.operationId + "Stream"); + break; + } + } + } + opInfo.put("hasSSE", hasSSE); + + serviceMap.get(serviceName).add(opInfo); + } + } + + // Convert service map to list for Mustache iteration + List> services = new ArrayList<>(); + for (Map.Entry>> entry : serviceMap.entrySet()) { + Map svc = new HashMap<>(); + svc.put("name", entry.getKey()); + svc.put("operations", entry.getValue()); + boolean serviceHasSSE = entry.getValue().stream() + .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); + svc.put("hasSSE", serviceHasSSE); + services.add(svc); + } + bundle.put("services", services); + + // Collect all operations flat + List> allOperations = new ArrayList<>(); + for (Map svc : services) { + List> ops = (List>) svc.get("operations"); + allOperations.addAll(ops); + } + allOperations.sort(Comparator.comparing(o -> (String) o.get("operationId"))); + bundle.put("allOperations", allOperations); + + boolean anySSE = allOperations.stream() + .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); + bundle.put("hasAnySSE", anySSE); + + // Topologically sort models + List models = (List) bundle.get("models"); + if (models != null) { + bundle.put("models", TypeScriptExpressZodUtils.topologicallySortModels(models)); + } + + // Inspect ModelError schema and expose error-shape template variables + if (models != null) { + for (Object modelEntry : models) { + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm != null && "ModelError".equals(cm.classname)) { + // Determine the message field name: prefer "message", then "title", + // then first required string property, fallback to "message" + String messageField = null; + String firstRequiredString = null; + boolean hasCode = false; + boolean hasSource = false; + + for (CodegenProperty prop : cm.vars) { + if ("code".equals(prop.name)) { + hasCode = true; + } + if ("source".equals(prop.name)) { + hasSource = true; + } + if (prop.required && prop.isString) { + if ("message".equals(prop.name)) { + messageField = "message"; + } else if ("title".equals(prop.name) && messageField == null) { + messageField = "title"; + } + if (firstRequiredString == null) { + firstRequiredString = prop.name; + } + } + } + + if (messageField == null) { + messageField = firstRequiredString != null ? firstRequiredString : "message"; + } + + bundle.put("errorMessageField", messageField); + bundle.put("errorHasCode", hasCode); + bundle.put("errorHasSource", hasSource); + break; + } + } + } + + // Flag whether any model is a discriminated union, so the DTO mappers can + // conditionally import ZodError (thrown from mapFrom*Dto default branches). + boolean hasDiscriminatedUnion = false; + if (models != null) { + for (Object modelEntry : models) { + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm != null && cm.vendorExtensions.containsKey("x-is-discriminated-union")) { + hasDiscriminatedUnion = true; + break; + } + } + } + bundle.put("hasDiscriminatedUnion", hasDiscriminatedUnion); + + return bundle; + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodServerCodegen.java similarity index 61% rename from modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java rename to modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodServerCodegen.java index 2eabe6b3010d..ca53b65647f8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodServerCodegen.java @@ -1,6 +1,5 @@ package org.openapitools.codegen.languages; -import io.swagger.v3.oas.models.media.Schema; import org.openapitools.codegen.*; import org.openapitools.codegen.meta.GeneratorMetadata; import org.openapitools.codegen.meta.Stability; @@ -8,7 +7,6 @@ import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.model.OperationMap; import org.openapitools.codegen.model.OperationsMap; -import org.openapitools.codegen.utils.ModelUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,10 +16,10 @@ import static org.openapitools.codegen.utils.StringUtils.camelize; -public class TypescriptExpressZodServerCodegen extends AbstractTypeScriptClientCodegen { - private final Logger LOGGER = LoggerFactory.getLogger(TypescriptExpressZodServerCodegen.class); +public class TypeScriptExpressZodServerCodegen extends AbstractTypeScriptClientCodegen { + private final Logger LOGGER = LoggerFactory.getLogger(TypeScriptExpressZodServerCodegen.class); - public TypescriptExpressZodServerCodegen() { + public TypeScriptExpressZodServerCodegen() { super(); generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) @@ -104,7 +102,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L } // Response Zod schema / wire type / mapper are resolved (array/map/primitive - // aware) in postProcessSupportingFileData via TypescriptExpressZodSupport. + // aware) in postProcessSupportingFileData via TypeScriptExpressZodUtils. // Mark whether this operation has only path params, only query params, etc. boolean hasOnlyPathParams = !op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; @@ -222,10 +220,10 @@ private String buildZodType(CodegenParameter param) { zod.append(".min(").append(param.minLength).append(")"); } } else if (param.isArray && param.items != null) { - zod.append(TypescriptExpressZodSupport.zodExpr(param.items)).append(".array()"); + zod.append(TypeScriptExpressZodUtils.zodExpr(param.items)).append(".array()"); } else if (param.isMap && param.additionalProperties != null) { zod.append("z.record(z.string(), ") - .append(TypescriptExpressZodSupport.zodExpr(param.additionalProperties)) + .append(TypeScriptExpressZodUtils.zodExpr(param.additionalProperties)) .append(")"); } else if (param.isModel || param.isBodyParam) { // Bodies whose schema dereferences to a bare primitive (e.g. a $ref'd enum @@ -301,210 +299,9 @@ private void buildRequestHandlerType(CodegenOperation op) { public Map postProcessAllModels(Map objs) { Map result = super.postProcessAllModels(objs); - // Collect enum model names (enums don't need DTO mappers) - Set enumModelNames = new HashSet<>(); - for (ModelsMap modEntry : result.values()) { - for (ModelMap innerMo : modEntry.getModels()) { - CodegenModel innerCm = innerMo.getModel(); - if (innerCm.isEnum) { - enumModelNames.add(innerCm.classname); - } - } - } - - for (ModelsMap entry : result.values()) { - for (ModelMap mo : entry.getModels()) { - CodegenModel cm = mo.getModel(); - - // Add Zod vendor extensions to each property - for (CodegenProperty prop : cm.vars) { - addZodVendorExtensions(prop, cm); - } - - // Look up original schema from OAS spec (shared by oneOf and allOf detection) - Map allSchemas = ModelUtils.getSchemas(this.openAPI); - Schema origSchema = null; - for (Map.Entry schemaEntry : allSchemas.entrySet()) { - if (toModelName(schemaEntry.getKey()).equals(cm.classname)) { - origSchema = schemaEntry.getValue(); - break; - } - } - - // Determine if this is a discriminated union - if (cm.discriminator != null && cm.oneOf != null && !cm.oneOf.isEmpty()) { - cm.vendorExtensions.put("x-is-discriminated-union", true); - cm.vendorExtensions.put("x-discriminator-property", cm.discriminator.getPropertyBaseName()); - - List> mappedTypes = new ArrayList<>(); - - if (cm.discriminator.getMappedModels() != null && !cm.discriminator.getMappedModels().isEmpty()) { - // Use explicit mappings from the discriminator - for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) { - Map mapped = new HashMap<>(); - mapped.put("modelName", mm.getModelName()); - mapped.put("mappingName", mm.getMappingName()); - mappedTypes.add(mapped); - } - } else { - // Build mappings from oneOf members by inspecting their discriminator property - for (String memberName : cm.oneOf) { - Map mapped = new HashMap<>(); - mapped.put("modelName", memberName); - // Try to find the discriminator value from the member model's literal property - String discValue = findDiscriminatorValue(memberName, cm.discriminator.getPropertyBaseName(), result); - mapped.put("mappingName", discValue != null ? discValue : memberName); - mappedTypes.add(mapped); - } - } - cm.vendorExtensions.put("x-mapped-models", mappedTypes); - } - - // Detect non-discriminated oneOf unions from the original OAS spec - if (cm.discriminator == null && !cm.vendorExtensions.containsKey("x-is-discriminated-union")) { - if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { - cm.vendorExtensions.put("x-is-union", true); - - // Collect member classnames - List memberNames = new ArrayList<>(); - for (Object memberObj : origSchema.getOneOf()) { - Schema memberSchema = (Schema) memberObj; - String ref = memberSchema.get$ref(); - if (ref != null) { - memberNames.add(toModelName(ModelUtils.getSimpleRef(ref))); - } - } - - // Collect property sets per member to find distinguishing properties - Map> memberPropSets = new LinkedHashMap<>(); - for (String memberName : memberNames) { - Set props = new LinkedHashSet<>(); - ModelsMap memberModels = result.get(memberName); - if (memberModels != null) { - for (ModelMap mm : memberModels.getModels()) { - for (CodegenProperty prop : mm.getModel().vars) { - props.add(prop.name); - } - } - } - memberPropSets.put(memberName, props); - } - - // Build x-union-members list with distinguishing properties - List> members = new ArrayList<>(); - for (int i = 0; i < memberNames.size(); i++) { - String memberName = memberNames.get(i); - Map info = new HashMap<>(); - info.put("modelName", memberName); - - if (i < memberNames.size() - 1) { - // Find a property unique to this member - Set myProps = memberPropSets.getOrDefault(memberName, Collections.emptySet()); - for (String prop : myProps) { - boolean unique = true; - for (Map.Entry> other : memberPropSets.entrySet()) { - if (!other.getKey().equals(memberName) && other.getValue().contains(prop)) { - unique = false; - break; - } - } - if (unique) { - info.put("hasDistinguishingProperty", true); - info.put("distinguishingProperty", prop); - break; - } - } - } - // Last member is always the fallback (no distinguishing property needed) - members.add(info); - } - - cm.vendorExtensions.put("x-union-members", members); - } - } - - // Detect allOf intersection types - if (origSchema != null && origSchema.getAllOf() != null && !origSchema.getAllOf().isEmpty()) { - List parentNames = new ArrayList<>(); - for (Object member : origSchema.getAllOf()) { - Schema memberSchema = (Schema) member; - if (memberSchema.get$ref() != null) { - parentNames.add(toModelName(ModelUtils.getSimpleRef(memberSchema.get$ref()))); - } - } - - if (!parentNames.isEmpty()) { - cm.vendorExtensions.put("x-is-intersection", true); - - List> parents = new ArrayList<>(); - Set parentPropertyNames = new HashSet<>(); - for (String parentName : parentNames) { - Map parentInfo = new HashMap<>(); - parentInfo.put("modelName", parentName); - parents.add(parentInfo); - - ModelsMap parentModels = result.get(parentName); - if (parentModels != null) { - for (ModelMap pmm : parentModels.getModels()) { - for (CodegenProperty prop : pmm.getModel().vars) { - parentPropertyNames.add(prop.name); - } - } - } - } - cm.vendorExtensions.put("x-intersection-parents", parents); - - boolean hasOwnProperties = false; - for (CodegenProperty prop : cm.vars) { - if (!parentPropertyNames.contains(prop.name)) { - prop.vendorExtensions.put("x-is-own-property", true); - hasOwnProperties = true; - } - } - cm.vendorExtensions.put("x-has-own-properties", hasOwnProperties); - } - } - - // Check if model has properties that reference other object models - // (not enums - enums are identity-mapped and don't need DTO mappers) - for (CodegenProperty prop : cm.vars) { - if (prop.complexType != null && !prop.isArray) { - if (enumModelNames.contains(prop.complexType)) { - // Reference to an enum type - mark for DTO template to use types. prefix - prop.vendorExtensions.put("x-is-enum-ref", true); - prop.vendorExtensions.put("x-enum-type", prop.complexType); - } else { - // Reference to an object type - needs DTO mapper - prop.vendorExtensions.put("x-is-ref", true); - prop.vendorExtensions.put("x-ref-type", prop.complexType); - } - } else if (prop.isArray && prop.items != null && prop.items.complexType != null) { - if (enumModelNames.contains(prop.items.complexType)) { - prop.vendorExtensions.put("x-is-enum-ref", true); - prop.vendorExtensions.put("x-enum-type", prop.items.complexType); - } else { - prop.vendorExtensions.put("x-is-ref", true); - prop.vendorExtensions.put("x-ref-type", prop.items.complexType); - } - } - } - - // Add DTO-related vendor extensions - cm.vendorExtensions.put("x-dto-name", cm.classname + "Dto"); - cm.vendorExtensions.put("x-schema-name", cm.classname + "Schema"); - - // Check if has single-value enum properties (literal types) - for (CodegenProperty prop : cm.vars) { - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - prop.vendorExtensions.put("x-is-literal", true); - prop.vendorExtensions.put("x-literal-value", values.get(0).toString()); - } - } - } - } - } + // Zod/union/intersection/ref annotations shared with the express-zod client + // (the `express-zod` framework of the `typescript` generator). + TypeScriptExpressZodUtils.annotateModels(result, this.openAPI, this::toModelName); // Second pass: Make discriminator properties required in Zod schemas. // Discriminator properties must not be .optional() so that the inferred @@ -540,31 +337,6 @@ public Map postProcessAllModels(Map objs) return result; } - /** - * Find the discriminator property's literal value in a member model. - * E.g., for UserLoginPrincipal with discriminator "kind", returns "USER_LOGIN". - */ - private String findDiscriminatorValue(String modelName, String discriminatorProperty, - Map allModels) { - ModelsMap modelsMap = allModels.get(modelName); - if (modelsMap == null) return null; - - for (ModelMap mo : modelsMap.getModels()) { - CodegenModel cm = mo.getModel(); - for (CodegenProperty prop : cm.vars) { - if (prop.baseName.equals(discriminatorProperty) || prop.name.equals(discriminatorProperty)) { - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - return values.get(0).toString(); - } - } - } - } - } - return null; - } - /** * Escape characters that are reserved in path-to-regexp v8 (used by Express v5). * Reserved chars: ( ) [ ] ? + ! @@ -597,33 +369,6 @@ private String escapeExpressReservedChars(String path) { return result.toString(); } - /** - * Add Zod-specific vendor extensions to a model property. - */ - private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { - String zodType = buildZodPropertyType(prop); - prop.vendorExtensions.put("x-zod-type", zodType); - - // Full expression including nullability and optionality. - String fullZod = zodType; - if (prop.isNullable) { - fullZod = fullZod + ".nullable()"; - } - if (!prop.required) { - fullZod = fullZod + ".optional()"; - } - prop.vendorExtensions.put("x-zod-full", fullZod); - } - - /** - * Build a Zod type expression for a model property. Delegates to the shared - * {@link TypescriptExpressZodSupport#zodExpr} so the server and client generators - * cannot diverge on the recursive array/map/primitive logic. - */ - private String buildZodPropertyType(CodegenProperty prop) { - return TypescriptExpressZodSupport.zodExpr(prop); - } - @Override @SuppressWarnings("unchecked") public Map postProcessSupportingFileData(Map bundle) { @@ -651,13 +396,13 @@ public Map postProcessSupportingFileData(Map bun String operationIdPascal = camelize(op.operationId); // Resolve response/body shapes (array/map/primitive aware). The server maps // domain results -> wire (TO_WIRE) and req.body wire -> domain (FROM_WIRE). - TypescriptExpressZodSupport.Resolved rr = op.returnType != null - ? TypescriptExpressZodSupport.resolveResponse( - op, TypescriptExpressZodSupport.Direction.TO_WIRE) + TypeScriptExpressZodUtils.Resolved rr = op.returnType != null + ? TypeScriptExpressZodUtils.resolveResponse( + op, TypeScriptExpressZodUtils.Direction.TO_WIRE) : null; - TypescriptExpressZodSupport.Resolved rb = op.bodyParam != null - ? TypescriptExpressZodSupport.resolveBody( - op.bodyParam, TypescriptExpressZodSupport.Direction.FROM_WIRE, + TypeScriptExpressZodUtils.Resolved rb = op.bodyParam != null + ? TypeScriptExpressZodUtils.resolveBody( + op.bodyParam, TypeScriptExpressZodUtils.Direction.FROM_WIRE, buildZodType(op.bodyParam)) : null; opInfo.put("operationId", op.operationId); @@ -852,7 +597,7 @@ public Map postProcessSupportingFileData(Map bun @SuppressWarnings("unchecked") List models = (List) bundle.get("models"); if (models != null) { - bundle.put("models", topologicallySortModels(models)); + bundle.put("models", TypeScriptExpressZodUtils.topologicallySortModels(models)); } // Flag whether any model is a discriminated union, so templates can @@ -873,92 +618,4 @@ public Map postProcessSupportingFileData(Map bun return bundle; } - - /** - * Topologically sort models so that leaf types (enums, simple objects) come before - * models that reference them. This ensures correct declaration order in generated schemas. - */ - @SuppressWarnings("unchecked") - private List topologicallySortModels(List models) { - // Build maps from classname to model entry and dependencies - Map modelByName = new LinkedHashMap<>(); - Map> dependencies = new LinkedHashMap<>(); - - for (Object modelEntry : models) { - Map entry = (Map) modelEntry; - CodegenModel cm = (CodegenModel) entry.get("model"); - if (cm == null) continue; - - modelByName.put(cm.classname, modelEntry); - - Set deps = new LinkedHashSet<>(); - for (CodegenProperty prop : cm.vars) { - if (prop.complexType != null) { - deps.add(prop.complexType); - } - if (prop.isArray && prop.items != null && prop.items.complexType != null) { - deps.add(prop.items.complexType); - } - } - // oneOf dependencies - if (cm.oneOf != null) { - deps.addAll(cm.oneOf); - } - // Also include non-discriminated union member dependencies - if (cm.vendorExtensions.containsKey("x-union-members")) { - @SuppressWarnings("unchecked") - List> unionMembers = - (List>) cm.vendorExtensions.get("x-union-members"); - for (Map member : unionMembers) { - deps.add((String) member.get("modelName")); - } - } - // Also include allOf intersection parent dependencies - if (cm.vendorExtensions.containsKey("x-intersection-parents")) { - @SuppressWarnings("unchecked") - List> intersectionParents = - (List>) cm.vendorExtensions.get("x-intersection-parents"); - for (Map parent : intersectionParents) { - deps.add(parent.get("modelName")); - } - } - // Remove self-references - deps.remove(cm.classname); - dependencies.put(cm.classname, deps); - } - - // DFS-based topological sort - List sorted = new ArrayList<>(); - Set visited = new LinkedHashSet<>(); - Set visiting = new LinkedHashSet<>(); - - for (String name : modelByName.keySet()) { - if (!visited.contains(name)) { - topoVisit(name, modelByName, dependencies, visited, visiting, sorted); - } - } - - return sorted; - } - - private void topoVisit(String name, Map modelByName, - Map> dependencies, - Set visited, Set visiting, List sorted) { - if (visited.contains(name)) return; - if (visiting.contains(name)) return; // cycle detected, break it - visiting.add(name); - - Set deps = dependencies.getOrDefault(name, Collections.emptySet()); - for (String dep : deps) { - if (modelByName.containsKey(dep)) { - topoVisit(dep, modelByName, dependencies, visited, visiting, sorted); - } - } - - visiting.remove(name); - visited.add(name); - if (modelByName.containsKey(name)) { - sorted.add(modelByName.get(name)); - } - } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java new file mode 100644 index 000000000000..1a25afa38f12 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptExpressZodUtils.java @@ -0,0 +1,645 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.CodegenDiscriminator; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.utils.ModelUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +/** + * Shared helpers for the {@code typescript-express-zod-server} generator and the + * {@code express-zod} framework of the unified {@code typescript} generator + * ({@link TypeScriptClientCodegen}). + * + *

The server generator and the client framework live in different classes, so the + * recursive Zod builder, the model annotation pass, the topological model sort, and the + * array/map/primitive DTO resolution would otherwise be duplicated and free to diverge. + * Keeping that logic here — the highest-risk, must-stay-identical part — is the single + * source of truth. Client-only post-processing lives in + * {@link TypeScriptExpressZodClientUtils}. + */ +final class TypeScriptExpressZodUtils { + private TypeScriptExpressZodUtils() { + } + + /** Direction of a DTO mapping: domain->wire ({@code mapTo*Dto}) or wire->domain ({@code mapFrom*Dto}). */ + enum Direction { + TO_WIRE, FROM_WIRE + } + + /** Resolution of a body/response shape into its Zod schema, wire (DTO) type, and mapper. */ + static final class Resolved { + /** Zod schema expression, e.g. {@code PetSchema.array()}. */ + final String zod; + /** Over-the-wire TypeScript type, e.g. {@code dtos.PetDto[]}. */ + final String dtoGeneric; + /** Domain TypeScript type qualified for use outside {@code types.ts}, e.g. {@code types.Pet[]}. */ + final String domainGeneric; + /** Function expression converting between domain and wire, or {@code null} for identity/passthrough. */ + final String mapFn; + + Resolved(String zod, String dtoGeneric, String domainGeneric, String mapFn) { + this.zod = zod; + this.dtoGeneric = dtoGeneric; + this.domainGeneric = domainGeneric; + this.mapFn = mapFn; + } + } + + // --------------------------------------------------------------------- + // Recursive Zod expression builder + // --------------------------------------------------------------------- + + /** + * Build a Zod type expression for a model/response property. Handles enums, arrays, + * maps ({@code z.record}), $refs, and primitives (with numeric/length constraints). + */ + static String zodExpr(CodegenProperty prop) { + if (prop == null) { + return "z.any()"; + } + + // Enum properties with allowable values. + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + // Single-value enum = literal. + return "z.literal(\"" + values.get(0) + "\")"; + } + if (prop.complexType != null) { + // Multi-value enum referenced via $ref — reference the enum schema. + return camelize(prop.complexType) + "Schema"; + } + if (values != null) { + StringBuilder sb = new StringBuilder("z.enum(["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("\"").append(values.get(i)).append("\""); + } + sb.append("])"); + return sb.toString(); + } + } + + // Arrays (before the complexType check, to handle Array). + // Nullable items get `.nullable()` to match the TS type (`Array` + // from getTypeDeclaration); top-level nullability of model properties is + // applied externally via x-zod-full. + if (prop.isArray && prop.items != null) { + String item = zodExpr(prop.items); + if (prop.items.isNullable) item += ".nullable()"; + return item + ".array()"; + } + + // Maps / free-form objects with a typed value (additionalProperties). Zod v4 + // requires the two-arg z.record(keyType, valueType) signature. + if (prop.isMap && prop.additionalProperties != null) { + String value = zodExpr(prop.additionalProperties); + if (prop.additionalProperties.isNullable) value += ".nullable()"; + return "z.record(z.string(), " + value + ")"; + } + + // $ref to another model or enum type. + if (prop.complexType != null) { + return camelize(prop.complexType) + "Schema"; + } + + // Primitive types. + if (prop.isInteger || prop.isLong) { + StringBuilder sb = new StringBuilder("z.coerce.number().int()"); + if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); + if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); + return sb.toString(); + } + if (prop.isNumber || prop.isFloat || prop.isDouble) { + StringBuilder sb = new StringBuilder("z.number()"); + if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); + if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); + return sb.toString(); + } + if (prop.isBoolean) { + return "z.boolean()"; + } + if (prop.isString) { + StringBuilder sb = new StringBuilder("z.string()"); + if (prop.maxLength != null) sb.append(".max(").append(prop.maxLength).append(")"); + if (prop.minLength != null) sb.append(".min(").append(prop.minLength).append(")"); + return sb.toString(); + } + + // Fallback. + return "z.any()"; + } + + // --------------------------------------------------------------------- + // Shared model post-processing + // --------------------------------------------------------------------- + + /** + * Annotate all models with the vendor extensions the express-zod templates rely on: + * per-property Zod expressions ({@code x-zod-type}/{@code x-zod-full}), discriminated + * unions ({@code x-is-discriminated-union}/{@code x-mapped-models}), non-discriminated + * oneOf unions ({@code x-is-union}/{@code x-union-members}), allOf intersections + * ({@code x-is-intersection}/{@code x-intersection-parents}/{@code x-has-own-properties}), + * enum/object $ref flags, DTO/schema names, and single-value enum literals. + * + *

{@code openAPI} and {@code toModelName} are passed in because this class has no + * access to the codegen's protected state; callers use {@code this.openAPI} and + * {@code this::toModelName}. + */ + static void annotateModels(Map result, OpenAPI openAPI, + Function toModelName) { + // Collect enum model names (enums don't need DTO mappers) + Set enumModelNames = new HashSet<>(); + for (ModelsMap modEntry : result.values()) { + for (ModelMap innerMo : modEntry.getModels()) { + CodegenModel innerCm = innerMo.getModel(); + if (innerCm.isEnum) { + enumModelNames.add(innerCm.classname); + } + } + } + + for (ModelsMap entry : result.values()) { + for (ModelMap mo : entry.getModels()) { + CodegenModel cm = mo.getModel(); + + // Add Zod vendor extensions to each property + for (CodegenProperty prop : cm.vars) { + addZodVendorExtensions(prop); + } + + // Look up original schema from OAS spec (shared by oneOf and allOf detection) + Map allSchemas = ModelUtils.getSchemas(openAPI); + Schema origSchema = null; + for (Map.Entry schemaEntry : allSchemas.entrySet()) { + if (toModelName.apply(schemaEntry.getKey()).equals(cm.classname)) { + origSchema = schemaEntry.getValue(); + break; + } + } + + // Determine if this is a discriminated union + if (cm.discriminator != null && cm.oneOf != null && !cm.oneOf.isEmpty()) { + cm.vendorExtensions.put("x-is-discriminated-union", true); + cm.vendorExtensions.put("x-discriminator-property", cm.discriminator.getPropertyBaseName()); + + List> mappedTypes = new ArrayList<>(); + + if (cm.discriminator.getMappedModels() != null && !cm.discriminator.getMappedModels().isEmpty()) { + // Use explicit mappings from the discriminator + for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) { + Map mapped = new HashMap<>(); + mapped.put("modelName", mm.getModelName()); + mapped.put("mappingName", mm.getMappingName()); + mappedTypes.add(mapped); + } + } else { + // Build mappings from oneOf members by inspecting their discriminator property + for (String memberName : cm.oneOf) { + Map mapped = new HashMap<>(); + mapped.put("modelName", memberName); + // Try to find the discriminator value from the member model's literal property + String discValue = findDiscriminatorValue(memberName, cm.discriminator.getPropertyBaseName(), result); + mapped.put("mappingName", discValue != null ? discValue : memberName); + mappedTypes.add(mapped); + } + } + cm.vendorExtensions.put("x-mapped-models", mappedTypes); + } + + // Detect non-discriminated oneOf unions from the original OAS spec + if (cm.discriminator == null && !cm.vendorExtensions.containsKey("x-is-discriminated-union")) { + if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { + cm.vendorExtensions.put("x-is-union", true); + + // Collect member classnames + List memberNames = new ArrayList<>(); + for (Object memberObj : origSchema.getOneOf()) { + Schema memberSchema = (Schema) memberObj; + String ref = memberSchema.get$ref(); + if (ref != null) { + memberNames.add(toModelName.apply(ModelUtils.getSimpleRef(ref))); + } + } + + // Collect property sets per member to find distinguishing properties + Map> memberPropSets = new LinkedHashMap<>(); + for (String memberName : memberNames) { + Set props = new LinkedHashSet<>(); + ModelsMap memberModels = result.get(memberName); + if (memberModels != null) { + for (ModelMap mm : memberModels.getModels()) { + for (CodegenProperty prop : mm.getModel().vars) { + props.add(prop.name); + } + } + } + memberPropSets.put(memberName, props); + } + + // Build x-union-members list with distinguishing properties + List> members = new ArrayList<>(); + for (int i = 0; i < memberNames.size(); i++) { + String memberName = memberNames.get(i); + Map info = new HashMap<>(); + info.put("modelName", memberName); + + if (i < memberNames.size() - 1) { + // Find a property unique to this member + Set myProps = memberPropSets.getOrDefault(memberName, Collections.emptySet()); + for (String prop : myProps) { + boolean unique = true; + for (Map.Entry> other : memberPropSets.entrySet()) { + if (!other.getKey().equals(memberName) && other.getValue().contains(prop)) { + unique = false; + break; + } + } + if (unique) { + info.put("hasDistinguishingProperty", true); + info.put("distinguishingProperty", prop); + break; + } + } + } + // Last member is always the fallback (no distinguishing property needed) + members.add(info); + } + + cm.vendorExtensions.put("x-union-members", members); + } + } + + // Detect allOf intersection types + if (origSchema != null && origSchema.getAllOf() != null && !origSchema.getAllOf().isEmpty()) { + List parentNames = new ArrayList<>(); + for (Object member : origSchema.getAllOf()) { + Schema memberSchema = (Schema) member; + if (memberSchema.get$ref() != null) { + parentNames.add(toModelName.apply(ModelUtils.getSimpleRef(memberSchema.get$ref()))); + } + } + + if (!parentNames.isEmpty()) { + cm.vendorExtensions.put("x-is-intersection", true); + + List> parents = new ArrayList<>(); + Set parentPropertyNames = new HashSet<>(); + for (String parentName : parentNames) { + Map parentInfo = new HashMap<>(); + parentInfo.put("modelName", parentName); + parents.add(parentInfo); + + ModelsMap parentModels = result.get(parentName); + if (parentModels != null) { + for (ModelMap pmm : parentModels.getModels()) { + for (CodegenProperty prop : pmm.getModel().vars) { + parentPropertyNames.add(prop.name); + } + } + } + } + cm.vendorExtensions.put("x-intersection-parents", parents); + + boolean hasOwnProperties = false; + for (CodegenProperty prop : cm.vars) { + if (!parentPropertyNames.contains(prop.name)) { + prop.vendorExtensions.put("x-is-own-property", true); + hasOwnProperties = true; + } + } + cm.vendorExtensions.put("x-has-own-properties", hasOwnProperties); + } + } + + // Check if model has properties that reference other object models + // (not enums - enums are identity-mapped and don't need DTO mappers) + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null && !prop.isArray) { + if (enumModelNames.contains(prop.complexType)) { + // Reference to an enum type - mark for DTO template to use types. prefix + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.complexType); + } else { + // Reference to an object type - needs DTO mapper + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.complexType); + } + } else if (prop.isArray && prop.items != null && prop.items.complexType != null) { + if (enumModelNames.contains(prop.items.complexType)) { + prop.vendorExtensions.put("x-is-enum-ref", true); + prop.vendorExtensions.put("x-enum-type", prop.items.complexType); + } else { + prop.vendorExtensions.put("x-is-ref", true); + prop.vendorExtensions.put("x-ref-type", prop.items.complexType); + } + } + } + + // Add DTO-related vendor extensions + cm.vendorExtensions.put("x-dto-name", cm.classname + "Dto"); + cm.vendorExtensions.put("x-schema-name", cm.classname + "Schema"); + + // Check if has single-value enum properties (literal types) + for (CodegenProperty prop : cm.vars) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + prop.vendorExtensions.put("x-is-literal", true); + prop.vendorExtensions.put("x-literal-value", values.get(0).toString()); + } + } + } + } + } + } + + /** + * Add Zod-specific vendor extensions to a model property: the bare Zod expression + * ({@code x-zod-type}) and the full expression including nullability and optionality + * ({@code x-zod-full}). + */ + private static void addZodVendorExtensions(CodegenProperty prop) { + String zodType = zodExpr(prop); + prop.vendorExtensions.put("x-zod-type", zodType); + + String fullZod = zodType; + if (prop.isNullable) { + fullZod = fullZod + ".nullable()"; + } + if (!prop.required) { + fullZod = fullZod + ".optional()"; + } + prop.vendorExtensions.put("x-zod-full", fullZod); + } + + /** + * Find the discriminator property's literal value in a member model. + * E.g., for UserLoginPrincipal with discriminator "kind", returns "USER_LOGIN". + */ + private static String findDiscriminatorValue(String modelName, String discriminatorProperty, + Map allModels) { + ModelsMap modelsMap = allModels.get(modelName); + if (modelsMap == null) return null; + + for (ModelMap mo : modelsMap.getModels()) { + CodegenModel cm = mo.getModel(); + for (CodegenProperty prop : cm.vars) { + if (prop.baseName.equals(discriminatorProperty) || prop.name.equals(discriminatorProperty)) { + if (prop.isEnum && prop.allowableValues != null) { + List values = (List) prop.allowableValues.get("values"); + if (values != null && values.size() == 1) { + return values.get(0).toString(); + } + } + } + } + } + return null; + } + + /** + * Topologically sort models so that leaf types (enums, simple objects) come before + * models that reference them. This ensures correct declaration order in generated schemas. + */ + @SuppressWarnings("unchecked") + static List topologicallySortModels(List models) { + // Build maps from classname to model entry and dependencies + Map modelByName = new LinkedHashMap<>(); + Map> dependencies = new LinkedHashMap<>(); + + for (Object modelEntry : models) { + Map entry = (Map) modelEntry; + CodegenModel cm = (CodegenModel) entry.get("model"); + if (cm == null) continue; + + modelByName.put(cm.classname, modelEntry); + + Set deps = new LinkedHashSet<>(); + for (CodegenProperty prop : cm.vars) { + if (prop.complexType != null) { + deps.add(prop.complexType); + } + if (prop.isArray && prop.items != null && prop.items.complexType != null) { + deps.add(prop.items.complexType); + } + } + // oneOf dependencies + if (cm.oneOf != null) { + deps.addAll(cm.oneOf); + } + // Also include non-discriminated union member dependencies + if (cm.vendorExtensions.containsKey("x-union-members")) { + List> unionMembers = + (List>) cm.vendorExtensions.get("x-union-members"); + for (Map member : unionMembers) { + deps.add((String) member.get("modelName")); + } + } + // Also include allOf intersection parent dependencies + if (cm.vendorExtensions.containsKey("x-intersection-parents")) { + List> intersectionParents = + (List>) cm.vendorExtensions.get("x-intersection-parents"); + for (Map parent : intersectionParents) { + deps.add(parent.get("modelName")); + } + } + // Remove self-references + deps.remove(cm.classname); + dependencies.put(cm.classname, deps); + } + + // DFS-based topological sort + List sorted = new ArrayList<>(); + Set visited = new LinkedHashSet<>(); + Set visiting = new LinkedHashSet<>(); + + for (String name : modelByName.keySet()) { + if (!visited.contains(name)) { + topoVisit(name, modelByName, dependencies, visited, visiting, sorted); + } + } + + return sorted; + } + + private static void topoVisit(String name, Map modelByName, + Map> dependencies, + Set visited, Set visiting, List sorted) { + if (visited.contains(name)) return; + if (visiting.contains(name)) return; // cycle detected, break it + visiting.add(name); + + Set deps = dependencies.getOrDefault(name, Collections.emptySet()); + for (String dep : deps) { + if (modelByName.containsKey(dep)) { + topoVisit(dep, modelByName, dependencies, visited, visiting, sorted); + } + } + + visiting.remove(name); + visited.add(name); + if (modelByName.containsKey(name)) { + sorted.add(modelByName.get(name)); + } + } + + // --------------------------------------------------------------------- + // Response / body shape resolution + // --------------------------------------------------------------------- + + /** + * Resolve an operation's response ({@code returnType}) into a Zod schema, wire type, + * and mapper. {@code dir} is {@code TO_WIRE} on the server (domain result -> wire) + * and {@code FROM_WIRE} on the client (wire json -> domain). + */ + static Resolved resolveResponse(CodegenOperation op, Direction dir) { + CodegenProperty rp = op.returnProperty; + String zod = zodExpr(rp); + if (rp == null) { + // DefaultCodegen assigns returnType and returnProperty together, so this is + // unreachable when callers guard on returnType != null; identity as a defensive + // fallback rather than fabricating a "Dto" symbol. + return new Resolved(zod, op.returnType, op.returnType, null); + } + Shape shape = shapeOf(rp, dir, 0); + return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); + } + + /** + * Resolve a request body parameter into a wire type and mapper. {@code zod} is the + * body's Zod expression (built by the caller's parameter builder). {@code dir} is + * {@code FROM_WIRE} on the server (req.body -> domain) and {@code TO_WIRE} on the + * client (domain -> wire before send). + */ + static Resolved resolveBody(CodegenParameter body, Direction dir, String zod) { + Shape shape; + if (body.isArray && body.items != null) { + shape = arrayShape(body.items, dir, 0); + } else if (body.isMap && body.additionalProperties != null) { + shape = mapShape(body.additionalProperties, dir, 0); + } else if (body.isModel) { + // For a scalar model body, CodegenParameter.dataType is the classname of the + // referenced model (e.g. "Pet"). Bodies that dereference to a primitive (e.g. + // a $ref'd enum handled as string by fromRequestBody) have isModel=false. + shape = new Shape("dtos." + body.dataType + "Dto", "types." + body.dataType, + "mappers." + mapPrefix(dir) + body.dataType + "Dto"); + } else { + // Bare primitive: domain == wire, identity mapping. + shape = new Shape(body.dataType, body.dataType, null); + } + return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); + } + + /** Wire type, domain type, and mapper (null => identity) for a resolved shape. */ + private static final class Shape { + final String dtoType; + final String domainType; + final String mapper; + + Shape(String dtoType, String domainType, String mapper) { + this.dtoType = dtoType; + this.domainType = domainType; + this.mapper = mapper; + } + } + + /** + * Recursively resolve a property's shape. Containers recurse into their element/value + * type (so nested shapes like {@code Array>} or map-of-array compose + * correctly); {@code depth} suffixes lambda parameters to avoid shadowing. + * + *

Note: container properties carry the innermost ref in {@code complexType} + * (DefaultCodegen.updatePropertyForArray/Map), so isArray/isMap MUST be checked + * before the complexType leaf branches. + */ + private static Shape shapeOf(CodegenProperty p, Direction dir, int depth) { + if (p.isArray && p.items != null) { + return arrayShape(p.items, dir, depth); + } + if (p.isMap && p.additionalProperties != null) { + return mapShape(p.additionalProperties, dir, depth); + } + if (p.complexType != null && !p.isEnum && !p.isEnumRef) { + // Object model — has a generated Dto + mappers. $ref'd enums set isEnumRef + // (not isEnum) and get neither, so they must not take this branch. + return new Shape("dtos." + p.complexType + "Dto", "types." + p.complexType, + "mappers." + mapPrefix(dir) + p.complexType + "Dto"); + } + if (p.complexType != null) { + // Enum (inline via isEnum or $ref via isEnumRef) — domain == wire, identity. + return new Shape("types." + p.complexType, "types." + p.complexType, null); + } + // Primitive or free-form object — domain == wire, identity. + return new Shape(p.dataType, p.dataType, null); + } + + private static Shape arrayShape(CodegenProperty items, Direction dir, int depth) { + Shape inner = shapeOf(items, dir, depth + 1); + String dtoElem = inner.dtoType; + String domainElem = inner.domainType; + String elemMapper = inner.mapper; + if (items.isNullable) { + // Match getTypeDeclaration's Array and skip nulls when mapping. + dtoElem = "(" + dtoElem + " | null)"; + domainElem = "(" + domainElem + " | null)"; + if (elemMapper != null) { + elemMapper = nullSafe(elemMapper, depth); + } + } + String mapper = elemMapper == null + ? null + : "(xs" + depth + ") => xs" + depth + ".map(" + elemMapper + ")"; + return new Shape(dtoElem + "[]", domainElem + "[]", mapper); + } + + private static Shape mapShape(CodegenProperty values, Direction dir, int depth) { + Shape inner = shapeOf(values, dir, depth + 1); + String dtoVal = inner.dtoType; + String domainVal = inner.domainType; + String valMapper = inner.mapper; + if (values.isNullable) { + dtoVal = dtoVal + " | null"; + domainVal = domainVal + " | null"; + if (valMapper != null) { + valMapper = nullSafe(valMapper, depth); + } + } + String mapper = valMapper == null + ? null + : "(m" + depth + ") => Object.fromEntries(Object.entries(m" + depth + + ").map(([k" + depth + ", v" + depth + "]) => [k" + depth + + ", (" + valMapper + ")(v" + depth + ")]))"; + return new Shape("{ [key: string]: " + dtoVal + " }", + "{ [key: string]: " + domainVal + " }", mapper); + } + + /** Wrap a mapper so null/undefined elements pass through untouched. */ + private static String nullSafe(String mapper, int depth) { + String e = "e" + depth; + return "(" + e + ") => " + e + " == null ? " + e + " : (" + mapper + ")(" + e + ")"; + } + + private static String mapPrefix(Direction dir) { + return dir == Direction.TO_WIRE ? "mapTo" : "mapFrom"; + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java deleted file mode 100644 index 6c9fbbc64994..000000000000 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodClientCodegen.java +++ /dev/null @@ -1,802 +0,0 @@ -package org.openapitools.codegen.languages; - -import io.swagger.v3.oas.models.media.Schema; -import org.openapitools.codegen.*; -import org.openapitools.codegen.model.ApiInfoMap; -import org.openapitools.codegen.meta.GeneratorMetadata; -import org.openapitools.codegen.meta.Stability; -import org.openapitools.codegen.model.ModelMap; -import org.openapitools.codegen.model.ModelsMap; -import org.openapitools.codegen.model.OperationMap; -import org.openapitools.codegen.model.OperationsMap; -import org.openapitools.codegen.utils.ModelUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.*; -import java.util.stream.Collectors; - -import static org.openapitools.codegen.utils.StringUtils.camelize; - -public class TypescriptExpressZodClientCodegen extends AbstractTypeScriptClientCodegen { - private final Logger LOGGER = LoggerFactory.getLogger(TypescriptExpressZodClientCodegen.class); - - public TypescriptExpressZodClientCodegen() { - super(); - - generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) - .stability(Stability.EXPERIMENTAL) - .build(); - - outputFolder = "generated-code" + File.separator + "typescript-express-zod-client"; - embeddedTemplateDir = templateDir = "typescript-express-zod-client"; - - // We generate everything via supporting files, not per-model/per-api - modelTemplateFiles.put("model.mustache", ".ts"); - apiTemplateFiles.put("api.mustache", ".ts"); - - apiPackage = ""; - modelPackage = ""; - } - - @Override - public CodegenType getTag() { - return CodegenType.CLIENT; - } - - @Override - public String getName() { - return "typescript-express-zod-client"; - } - - @Override - public String getHelp() { - return "Generates a TypeScript HTTP client with Zod response validation for an Express API."; - } - - @Override - public void processOpts() { - super.processOpts(); - - supportingFiles.clear(); - modelTemplateFiles.clear(); - apiTemplateFiles.clear(); - - supportingFiles.add(new SupportingFile("types.mustache", "", "types.ts")); - supportingFiles.add(new SupportingFile("schemas.mustache", "", "schemas.ts")); - supportingFiles.add(new SupportingFile("dtos" + File.separator + "types.mustache", "dtos", "types.ts")); - supportingFiles.add(new SupportingFile("dtos" + File.separator + "mappers.mustache", "dtos", "mappers.ts")); - supportingFiles.add(new SupportingFile("dtos" + File.separator + "index.mustache", "dtos", "index.ts")); - supportingFiles.add(new SupportingFile("http-client.mustache", "", "http-client.ts")); - supportingFiles.add(new SupportingFile("index.mustache", "", "index.ts")); - } - - @Override - public OperationsMap postProcessOperationsWithModels(OperationsMap operations, List allModels) { - OperationMap objs = operations.getOperations(); - List ops = objs.getOperation(); - - for (CodegenOperation op : ops) { - // Build URL template for JS template literals: - // /recipe/{recipeId} -> /recipe/${encodeURIComponent(params.recipeId)} - String urlTemplate = op.path.replaceAll( - "\\{([^}]+)\\}", - "\\${encodeURIComponent(String(params.$1))}" - ); - op.vendorExtensions.put("x-url-template", urlTemplate); - - // Determine the service name from the tag - if (op.tags != null && !op.tags.isEmpty()) { - String tag = op.tags.get(0).getName(); - String serviceName = camelize(tag) + "Service"; - op.vendorExtensions.put("x-service-name", serviceName); - } - - // Set fetch method only for non-GET (GET is fetch default) - String httpMethodUpper = op.httpMethod.toUpperCase(Locale.ENGLISH); - if (!"GET".equals(httpMethodUpper)) { - op.vendorExtensions.put("x-fetch-method", httpMethodUpper); - } - - // Response Zod schema / wire type / mapper are resolved (array/map/primitive - // aware) in postProcessSupportingFileData via TypescriptExpressZodSupport. - - // Build params type name - String paramsType = camelize(op.operationId) + "Params"; - op.vendorExtensions.put("x-params-type", paramsType); - - // Check if all params are optional - boolean allParamsOptional = true; - for (CodegenParameter param : op.allParams) { - if (param.required) { - allParamsOptional = false; - break; - } - } - op.vendorExtensions.put("x-all-params-optional", allParamsOptional); - - // Body param info - boolean hasBodyParam = op.bodyParam != null; - op.vendorExtensions.put("x-has-body-param", hasBodyParam); - if (hasBodyParam) { - String bodyParamName = op.bodyParam.paramName; - if (bodyParamName == null || bodyParamName.isEmpty()) { - bodyParamName = "body"; - } - op.vendorExtensions.put("x-body-param-name", bodyParamName); - } - - boolean hasNoParams = op.pathParams.isEmpty() && op.queryParams.isEmpty() && op.bodyParam == null; - op.vendorExtensions.put("x-has-no-params", hasNoParams); - - // Add vendor extensions to each parameter for Zod schema building - for (CodegenParameter param : op.allParams) { - addZodVendorExtensions(param); - } - } - - return operations; - } - - /** - * Add Zod-specific vendor extensions to a parameter. - */ - private void addZodVendorExtensions(CodegenParameter param) { - String zodType = buildZodType(param); - param.vendorExtensions.put("x-zod-type", zodType); - - // Track if the query param name differs from the camelCase name - if (param.isQueryParam && !param.baseName.equals(param.paramName)) { - param.vendorExtensions.put("x-original-name", param.baseName); - param.vendorExtensions.put("x-has-different-name", true); - } - } - - /** - * Build a Zod type expression for a parameter. - */ - private String buildZodType(CodegenParameter param) { - StringBuilder zod = new StringBuilder(); - - if (param.isEnum) { - zod.append(camelize(param.dataType) + "Schema"); - } else if (!param.isPrimitiveType && param.dataType != null && !param.isArray - && !param.isBodyParam && !param.isModel) { - zod.append(camelize(param.dataType) + "Schema"); - } else if (param.isInteger || param.isLong) { - zod.append("z.coerce.number().int()"); - if (param.minimum != null) { - zod.append(".gte(").append(param.minimum).append(")"); - } - if (param.maximum != null) { - zod.append(".lte(").append(param.maximum).append(")"); - } - } else if (param.isNumber || param.isFloat || param.isDouble) { - zod.append("z.number()"); - if (param.minimum != null) { - zod.append(".gte(").append(param.minimum).append(")"); - } - if (param.maximum != null) { - zod.append(".lte(").append(param.maximum).append(")"); - } - } else if (param.isBoolean) { - zod.append("z.boolean()"); - } else if (param.isString) { - zod.append("z.string()"); - if (param.maxLength != null) { - zod.append(".max(").append(param.maxLength).append(")"); - } - if (param.minLength != null) { - zod.append(".min(").append(param.minLength).append(")"); - } - } else if (param.isArray && param.items != null) { - zod.append(TypescriptExpressZodSupport.zodExpr(param.items)).append(".array()"); - } else if (param.isMap && param.additionalProperties != null) { - zod.append("z.record(z.string(), ") - .append(TypescriptExpressZodSupport.zodExpr(param.additionalProperties)) - .append(")"); - } else if (param.isModel || param.isBodyParam) { - // Bodies whose schema dereferences to a bare primitive (e.g. a $ref'd enum - // handled as string by fromRequestBody) carry a primitive dataType with the - // primitive flags unset — camelize(dataType)+"Schema" would fabricate an - // unresolved symbol like StringSchema. - if ("string".equals(param.dataType)) { - zod.append("z.string()"); - } else if ("number".equals(param.dataType)) { - zod.append("z.number()"); - } else if ("boolean".equals(param.dataType)) { - zod.append("z.boolean()"); - } else { - zod.append(camelize(param.dataType) + "Schema"); - } - } else { - zod.append("z.string()"); - if (param.maxLength != null) { - zod.append(".max(").append(param.maxLength).append(")"); - } - } - - if (!param.required) { - zod.append(".optional()"); - } - - return zod.toString(); - } - - @Override - public Map postProcessAllModels(Map objs) { - Map result = super.postProcessAllModels(objs); - - // Collect enum model names - Set enumModelNames = new HashSet<>(); - for (ModelsMap modEntry : result.values()) { - for (ModelMap innerMo : modEntry.getModels()) { - CodegenModel innerCm = innerMo.getModel(); - if (innerCm.isEnum) { - enumModelNames.add(innerCm.classname); - } - } - } - - for (ModelsMap entry : result.values()) { - for (ModelMap mo : entry.getModels()) { - CodegenModel cm = mo.getModel(); - - // Add Zod vendor extensions to each property - for (CodegenProperty prop : cm.vars) { - addZodVendorExtensions(prop, cm); - } - - // Look up original schema from OAS spec (shared by oneOf and allOf detection) - Map allSchemas = ModelUtils.getSchemas(this.openAPI); - Schema origSchema = null; - for (Map.Entry schemaEntry : allSchemas.entrySet()) { - if (toModelName(schemaEntry.getKey()).equals(cm.classname)) { - origSchema = schemaEntry.getValue(); - break; - } - } - - // Determine if this is a discriminated union - if (cm.discriminator != null && cm.oneOf != null && !cm.oneOf.isEmpty()) { - cm.vendorExtensions.put("x-is-discriminated-union", true); - cm.vendorExtensions.put("x-discriminator-property", cm.discriminator.getPropertyBaseName()); - - List> mappedTypes = new ArrayList<>(); - - if (cm.discriminator.getMappedModels() != null && !cm.discriminator.getMappedModels().isEmpty()) { - for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) { - Map mapped = new HashMap<>(); - mapped.put("modelName", mm.getModelName()); - mapped.put("mappingName", mm.getMappingName()); - mappedTypes.add(mapped); - } - } else { - for (String memberName : cm.oneOf) { - Map mapped = new HashMap<>(); - mapped.put("modelName", memberName); - String discValue = findDiscriminatorValue(memberName, cm.discriminator.getPropertyBaseName(), result); - mapped.put("mappingName", discValue != null ? discValue : memberName); - mappedTypes.add(mapped); - } - } - cm.vendorExtensions.put("x-mapped-models", mappedTypes); - } - - // Detect non-discriminated oneOf unions from the original OAS spec - if (cm.discriminator == null && !cm.vendorExtensions.containsKey("x-is-discriminated-union")) { - if (origSchema != null && origSchema.getOneOf() != null && !origSchema.getOneOf().isEmpty()) { - cm.vendorExtensions.put("x-is-union", true); - - // Collect member classnames - List memberNames = new ArrayList<>(); - for (Object memberObj : origSchema.getOneOf()) { - Schema memberSchema = (Schema) memberObj; - String ref = memberSchema.get$ref(); - if (ref != null) { - memberNames.add(toModelName(ModelUtils.getSimpleRef(ref))); - } - } - - // Collect property sets per member to find distinguishing properties - Map> memberPropSets = new LinkedHashMap<>(); - for (String memberName : memberNames) { - Set props = new LinkedHashSet<>(); - ModelsMap memberModels = result.get(memberName); - if (memberModels != null) { - for (ModelMap mm : memberModels.getModels()) { - for (CodegenProperty prop : mm.getModel().vars) { - props.add(prop.name); - } - } - } - memberPropSets.put(memberName, props); - } - - // Build x-union-members list with distinguishing properties - List> members = new ArrayList<>(); - for (int i = 0; i < memberNames.size(); i++) { - String memberName = memberNames.get(i); - Map info = new HashMap<>(); - info.put("modelName", memberName); - - if (i < memberNames.size() - 1) { - // Find a property unique to this member - Set myProps = memberPropSets.getOrDefault(memberName, Collections.emptySet()); - for (String prop : myProps) { - boolean unique = true; - for (Map.Entry> other : memberPropSets.entrySet()) { - if (!other.getKey().equals(memberName) && other.getValue().contains(prop)) { - unique = false; - break; - } - } - if (unique) { - info.put("hasDistinguishingProperty", true); - info.put("distinguishingProperty", prop); - break; - } - } - } - // Last member is always the fallback (no distinguishing property needed) - members.add(info); - } - - cm.vendorExtensions.put("x-union-members", members); - } - } - - // Detect allOf intersection types - if (origSchema != null && origSchema.getAllOf() != null && !origSchema.getAllOf().isEmpty()) { - List parentNames = new ArrayList<>(); - for (Object member : origSchema.getAllOf()) { - Schema memberSchema = (Schema) member; - if (memberSchema.get$ref() != null) { - parentNames.add(toModelName(ModelUtils.getSimpleRef(memberSchema.get$ref()))); - } - } - - if (!parentNames.isEmpty()) { - cm.vendorExtensions.put("x-is-intersection", true); - - List> parents = new ArrayList<>(); - Set parentPropertyNames = new HashSet<>(); - for (String parentName : parentNames) { - Map parentInfo = new HashMap<>(); - parentInfo.put("modelName", parentName); - parents.add(parentInfo); - - ModelsMap parentModels = result.get(parentName); - if (parentModels != null) { - for (ModelMap pmm : parentModels.getModels()) { - for (CodegenProperty prop : pmm.getModel().vars) { - parentPropertyNames.add(prop.name); - } - } - } - } - cm.vendorExtensions.put("x-intersection-parents", parents); - - boolean hasOwnProperties = false; - for (CodegenProperty prop : cm.vars) { - if (!parentPropertyNames.contains(prop.name)) { - prop.vendorExtensions.put("x-is-own-property", true); - hasOwnProperties = true; - } - } - cm.vendorExtensions.put("x-has-own-properties", hasOwnProperties); - } - } - - // Check if model has properties that reference other object models or enums - for (CodegenProperty prop : cm.vars) { - if (prop.complexType != null && !prop.isArray) { - if (enumModelNames.contains(prop.complexType)) { - prop.vendorExtensions.put("x-is-enum-ref", true); - prop.vendorExtensions.put("x-enum-type", prop.complexType); - } else { - prop.vendorExtensions.put("x-is-ref", true); - prop.vendorExtensions.put("x-ref-type", prop.complexType); - } - } else if (prop.isArray && prop.items != null && prop.items.complexType != null) { - if (enumModelNames.contains(prop.items.complexType)) { - prop.vendorExtensions.put("x-is-enum-ref", true); - prop.vendorExtensions.put("x-enum-type", prop.items.complexType); - } else { - prop.vendorExtensions.put("x-is-ref", true); - prop.vendorExtensions.put("x-ref-type", prop.items.complexType); - } - } - } - - // Schema and DTO names - cm.vendorExtensions.put("x-schema-name", cm.classname + "Schema"); - cm.vendorExtensions.put("x-dto-name", cm.classname + "Dto"); - - // Check if has single-value enum properties (literal types) - for (CodegenProperty prop : cm.vars) { - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - prop.vendorExtensions.put("x-is-literal", true); - prop.vendorExtensions.put("x-literal-value", values.get(0).toString()); - } - } - } - } - } - - return result; - } - - /** - * Find the discriminator property's literal value in a member model. - */ - private String findDiscriminatorValue(String modelName, String discriminatorProperty, - Map allModels) { - ModelsMap modelsMap = allModels.get(modelName); - if (modelsMap == null) return null; - - for (ModelMap mo : modelsMap.getModels()) { - CodegenModel cm = mo.getModel(); - for (CodegenProperty prop : cm.vars) { - if (prop.baseName.equals(discriminatorProperty) || prop.name.equals(discriminatorProperty)) { - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - return values.get(0).toString(); - } - } - } - } - } - return null; - } - - /** - * Add Zod-specific vendor extensions to a model property. - */ - private void addZodVendorExtensions(CodegenProperty prop, CodegenModel model) { - String zodType = buildZodPropertyType(prop); - prop.vendorExtensions.put("x-zod-type", zodType); - - String fullZod = zodType; - if (prop.isNullable) { - fullZod = fullZod + ".nullable()"; - } - if (!prop.required) { - fullZod = fullZod + ".optional()"; - } - prop.vendorExtensions.put("x-zod-full", fullZod); - } - - /** - * Build a Zod type expression for a model property. Delegates to the shared - * {@link TypescriptExpressZodSupport#zodExpr} so the server and client generators - * cannot diverge on the recursive array/map/primitive logic. - */ - private String buildZodPropertyType(CodegenProperty prop) { - return TypescriptExpressZodSupport.zodExpr(prop); - } - - @Override - @SuppressWarnings("unchecked") - public Map postProcessSupportingFileData(Map bundle) { - bundle = super.postProcessSupportingFileData(bundle); - - List allOps = ((org.openapitools.codegen.model.ApiInfoMap) bundle.get("apiInfo")).getApis(); - - // Collect all unique services - Map>> serviceMap = new LinkedHashMap<>(); - - for (OperationsMap opsMap : allOps) { - OperationMap operationMap = opsMap.getOperations(); - List ops = operationMap.getOperation(); - - for (CodegenOperation op : ops) { - String serviceName = (String) op.vendorExtensions.getOrDefault("x-service-name", "DefaultService"); - - serviceMap.computeIfAbsent(serviceName, k -> new ArrayList<>()); - - Map opInfo = new HashMap<>(); - String operationIdPascal = camelize(op.operationId); - // Resolve response/body shapes (array/map/primitive aware). The client converts - // wire json -> domain (FROM_WIRE) and domain -> wire before sending (TO_WIRE). - TypescriptExpressZodSupport.Resolved rr = op.returnType != null - ? TypescriptExpressZodSupport.resolveResponse( - op, TypescriptExpressZodSupport.Direction.FROM_WIRE) - : null; - TypescriptExpressZodSupport.Resolved rb = op.bodyParam != null - ? TypescriptExpressZodSupport.resolveBody( - op.bodyParam, TypescriptExpressZodSupport.Direction.TO_WIRE, - buildZodType(op.bodyParam)) - : null; - opInfo.put("operationId", op.operationId); - opInfo.put("operationIdPascal", operationIdPascal); - opInfo.put("description", op.summary != null ? op.summary : op.notes); - opInfo.put("httpMethod", op.httpMethod.toLowerCase(Locale.ENGLISH)); - opInfo.put("httpMethodUpper", op.httpMethod.toUpperCase(Locale.ENGLISH)); - opInfo.put("serviceName", serviceName); - opInfo.put("returnType", op.returnType); - opInfo.put("hasParams", !op.allParams.isEmpty()); - opInfo.put("hasNoParams", op.allParams.isEmpty()); - opInfo.put("allParamsOptional", op.vendorExtensions.get("x-all-params-optional")); - - // URL template for JS template literals - opInfo.put("urlTemplate", op.vendorExtensions.get("x-url-template")); - - // Fetch method (only for non-GET) - opInfo.put("fetchMethod", op.vendorExtensions.get("x-fetch-method")); - opInfo.put("hasFetchMethod", op.vendorExtensions.containsKey("x-fetch-method")); - - // Params info - List> pathParams = new ArrayList<>(); - for (CodegenParameter p : op.pathParams) { - Map pm = new HashMap<>(); - pm.put("name", p.paramName); - pm.put("baseName", p.baseName); - pm.put("zodType", buildZodType(p)); - pm.put("dataType", p.dataType); - pathParams.add(pm); - } - opInfo.put("pathParams", pathParams); - opInfo.put("hasPathParams", !pathParams.isEmpty()); - - List> queryParams = new ArrayList<>(); - for (CodegenParameter p : op.queryParams) { - Map qm = new HashMap<>(); - qm.put("name", p.paramName); - qm.put("baseName", p.baseName); - qm.put("hasDifferentName", !p.baseName.equals(p.paramName)); - qm.put("zodType", buildZodType(p)); - qm.put("dataType", p.dataType); - qm.put("required", p.required); - qm.put("isEnum", p.isEnum); - queryParams.add(qm); - } - opInfo.put("queryParams", queryParams); - opInfo.put("hasQueryParams", !queryParams.isEmpty()); - - if (op.bodyParam != null) { - Map body = new HashMap<>(); - body.put("name", op.vendorExtensions.get("x-body-param-name")); - body.put("dataType", op.bodyParam.dataType); - body.put("required", op.bodyParam.required); - opInfo.put("bodyParam", body); - opInfo.put("hasBodyParam", true); - // Convert the domain body -> wire before JSON.stringify. Identity (x) => x - // for bare primitives so the template can always call it as a function. - // When the body can be undefined (optional requestBody, or params itself - // optional), guard the mapper so absent bodies keep the old - // JSON.stringify(undefined) -> no-body behavior instead of crashing. - String bodyMapFn = rb.mapFn != null ? rb.mapFn : "(x) => x"; - boolean bodyMayBeUndefined = !op.bodyParam.required - || Boolean.TRUE.equals(op.vendorExtensions.get("x-all-params-optional")); - if (rb.mapFn != null && bodyMayBeUndefined) { - bodyMapFn = "(b) => b === undefined ? undefined : (" + rb.mapFn + ")(b)"; - } - opInfo.put("bodyMapFn", bodyMapFn); - } else { - opInfo.put("hasBodyParam", false); - } - - // Response info. The response Zod schema is emitted as a per-operation const - // in schemas.ts, and the parsed wire payload is converted to domain via mapFn - // (identity (x) => x for bare primitives, so it can always be called as a function). - opInfo.put("responseType", op.returnType); - if (rr != null) { - opInfo.put("responseSchema", operationIdPascal + "ResponseSchema"); - opInfo.put("responseZodExpr", rr.zod); - opInfo.put("responseDtoMapper", rr.mapFn != null ? rr.mapFn : "(x) => x"); - opInfo.put("responseDtoGeneric", rr.dtoGeneric); - // Domain type qualified for http-client.ts (which imports types); e.g. - // types.Pet[], { [key: string]: number }, string. - opInfo.put("responseDomainGeneric", rr.domainGeneric); - } - - // SSE detection - boolean hasSSE = false; - for (CodegenResponse response : op.responses) { - if (response.is2xx && response.getContent() != null) { - CodegenMediaType sseMt = response.getContent().get("text/event-stream"); - if (sseMt != null && sseMt.getSchema() != null) { - hasSSE = true; - String sseType = sseMt.getSchema().getComplexType(); - if (sseType == null) sseType = sseMt.getSchema().getDataType(); - opInfo.put("sseType", sseType); - opInfo.put("sseDtoType", sseType + "Dto"); - opInfo.put("sseDtoMapper", "mapFrom" + sseType + "Dto"); - opInfo.put("sseSchema", sseType + "Schema"); - opInfo.put("streamOperationId", op.operationId + "Stream"); - break; - } - } - } - opInfo.put("hasSSE", hasSSE); - - serviceMap.get(serviceName).add(opInfo); - } - } - - // Convert service map to list for Mustache iteration - List> services = new ArrayList<>(); - for (Map.Entry>> entry : serviceMap.entrySet()) { - Map svc = new HashMap<>(); - svc.put("name", entry.getKey()); - svc.put("operations", entry.getValue()); - boolean serviceHasSSE = entry.getValue().stream() - .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); - svc.put("hasSSE", serviceHasSSE); - services.add(svc); - } - bundle.put("services", services); - - // Collect all operations flat - List> allOperations = new ArrayList<>(); - for (Map svc : services) { - @SuppressWarnings("unchecked") - List> ops = (List>) svc.get("operations"); - allOperations.addAll(ops); - } - allOperations.sort(Comparator.comparing(o -> (String) o.get("operationId"))); - bundle.put("allOperations", allOperations); - - boolean anySSE = allOperations.stream() - .anyMatch(op -> Boolean.TRUE.equals(op.get("hasSSE"))); - bundle.put("hasAnySSE", anySSE); - - // Topologically sort models - @SuppressWarnings("unchecked") - List models = (List) bundle.get("models"); - if (models != null) { - bundle.put("models", topologicallySortModels(models)); - } - - // Inspect ModelError schema and expose error-shape template variables - if (models != null) { - for (Object modelEntry : models) { - @SuppressWarnings("unchecked") - Map entry = (Map) modelEntry; - CodegenModel cm = (CodegenModel) entry.get("model"); - if (cm != null && "ModelError".equals(cm.classname)) { - // Determine the message field name: prefer "message", then "title", - // then first required string property, fallback to "message" - String messageField = null; - String firstRequiredString = null; - boolean hasCode = false; - boolean hasSource = false; - - for (CodegenProperty prop : cm.vars) { - if ("code".equals(prop.name)) { - hasCode = true; - } - if ("source".equals(prop.name)) { - hasSource = true; - } - if (prop.required && prop.isString) { - if ("message".equals(prop.name)) { - messageField = "message"; - } else if ("title".equals(prop.name) && messageField == null) { - messageField = "title"; - } - if (firstRequiredString == null) { - firstRequiredString = prop.name; - } - } - } - - if (messageField == null) { - messageField = firstRequiredString != null ? firstRequiredString : "message"; - } - - bundle.put("errorMessageField", messageField); - bundle.put("errorHasCode", hasCode); - bundle.put("errorHasSource", hasSource); - break; - } - } - } - - // Flag whether any model is a discriminated union, so the DTO mappers can - // conditionally import ZodError (thrown from mapFrom*Dto default branches). - boolean hasDiscriminatedUnion = false; - if (models != null) { - for (Object modelEntry : models) { - @SuppressWarnings("unchecked") - Map entry = (Map) modelEntry; - CodegenModel cm = (CodegenModel) entry.get("model"); - if (cm != null && cm.vendorExtensions.containsKey("x-is-discriminated-union")) { - hasDiscriminatedUnion = true; - break; - } - } - } - bundle.put("hasDiscriminatedUnion", hasDiscriminatedUnion); - - return bundle; - } - - /** - * Topologically sort models so that leaf types come before models that reference them. - */ - @SuppressWarnings("unchecked") - private List topologicallySortModels(List models) { - Map modelByName = new LinkedHashMap<>(); - Map> dependencies = new LinkedHashMap<>(); - - for (Object modelEntry : models) { - Map entry = (Map) modelEntry; - CodegenModel cm = (CodegenModel) entry.get("model"); - if (cm == null) continue; - - modelByName.put(cm.classname, modelEntry); - - Set deps = new LinkedHashSet<>(); - for (CodegenProperty prop : cm.vars) { - if (prop.complexType != null) { - deps.add(prop.complexType); - } - if (prop.isArray && prop.items != null && prop.items.complexType != null) { - deps.add(prop.items.complexType); - } - } - if (cm.oneOf != null) { - deps.addAll(cm.oneOf); - } - // Also include non-discriminated union member dependencies - if (cm.vendorExtensions.containsKey("x-union-members")) { - @SuppressWarnings("unchecked") - List> unionMembers = - (List>) cm.vendorExtensions.get("x-union-members"); - for (Map member : unionMembers) { - deps.add((String) member.get("modelName")); - } - } - // Also include allOf intersection parent dependencies - if (cm.vendorExtensions.containsKey("x-intersection-parents")) { - @SuppressWarnings("unchecked") - List> intersectionParents = - (List>) cm.vendorExtensions.get("x-intersection-parents"); - for (Map parent : intersectionParents) { - deps.add(parent.get("modelName")); - } - } - deps.remove(cm.classname); - dependencies.put(cm.classname, deps); - } - - List sorted = new ArrayList<>(); - Set visited = new LinkedHashSet<>(); - Set visiting = new LinkedHashSet<>(); - - for (String name : modelByName.keySet()) { - if (!visited.contains(name)) { - topoVisit(name, modelByName, dependencies, visited, visiting, sorted); - } - } - - return sorted; - } - - private void topoVisit(String name, Map modelByName, - Map> dependencies, - Set visited, Set visiting, List sorted) { - if (visited.contains(name)) return; - if (visiting.contains(name)) return; // cycle detected, break it - visiting.add(name); - - Set deps = dependencies.getOrDefault(name, Collections.emptySet()); - for (String dep : deps) { - if (modelByName.containsKey(dep)) { - topoVisit(dep, modelByName, dependencies, visited, visiting, sorted); - } - } - - visiting.remove(name); - visited.add(name); - if (modelByName.containsKey(name)) { - sorted.add(modelByName.get(name)); - } - } -} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java deleted file mode 100644 index 30580d8addc8..000000000000 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypescriptExpressZodSupport.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.openapitools.codegen.languages; - -import org.openapitools.codegen.CodegenOperation; -import org.openapitools.codegen.CodegenParameter; -import org.openapitools.codegen.CodegenProperty; - -import java.util.List; - -import static org.openapitools.codegen.utils.StringUtils.camelize; - -/** - * Shared helpers for the {@code typescript-express-zod-server} and - * {@code typescript-express-zod-client} generators. - * - *

The two generators are siblings (both extend {@code AbstractTypeScriptClientCodegen}, - * neither extends the other), so the recursive Zod builder and the array/map/primitive - * DTO resolution would otherwise be duplicated and free to diverge. Keeping that logic - * here — the highest-risk, must-stay-identical part — is the single source of truth. - * - *

The per-generator {@code postProcess*} methods keep their own vendor-extension wiring - * (which keys they set, {@code dtos.}/{@code mappers.} imports) but delegate the string - * computation to these static helpers. - */ -final class TypescriptExpressZodSupport { - private TypescriptExpressZodSupport() { - } - - /** Direction of a DTO mapping: domain->wire ({@code mapTo*Dto}) or wire->domain ({@code mapFrom*Dto}). */ - enum Direction { - TO_WIRE, FROM_WIRE - } - - /** Resolution of a body/response shape into its Zod schema, wire (DTO) type, and mapper. */ - static final class Resolved { - /** Zod schema expression, e.g. {@code PetSchema.array()}. */ - final String zod; - /** Over-the-wire TypeScript type, e.g. {@code dtos.PetDto[]}. */ - final String dtoGeneric; - /** Domain TypeScript type qualified for use outside {@code types.ts}, e.g. {@code types.Pet[]}. */ - final String domainGeneric; - /** Function expression converting between domain and wire, or {@code null} for identity/passthrough. */ - final String mapFn; - - Resolved(String zod, String dtoGeneric, String domainGeneric, String mapFn) { - this.zod = zod; - this.dtoGeneric = dtoGeneric; - this.domainGeneric = domainGeneric; - this.mapFn = mapFn; - } - } - - // --------------------------------------------------------------------- - // Recursive Zod expression builder - // --------------------------------------------------------------------- - - /** - * Build a Zod type expression for a model/response property. Handles enums, arrays, - * maps ({@code z.record}), $refs, and primitives (with numeric/length constraints). - */ - static String zodExpr(CodegenProperty prop) { - if (prop == null) { - return "z.any()"; - } - - // Enum properties with allowable values. - if (prop.isEnum && prop.allowableValues != null) { - List values = (List) prop.allowableValues.get("values"); - if (values != null && values.size() == 1) { - // Single-value enum = literal. - return "z.literal(\"" + values.get(0) + "\")"; - } - if (prop.complexType != null) { - // Multi-value enum referenced via $ref — reference the enum schema. - return camelize(prop.complexType) + "Schema"; - } - if (values != null) { - StringBuilder sb = new StringBuilder("z.enum(["); - for (int i = 0; i < values.size(); i++) { - if (i > 0) sb.append(", "); - sb.append("\"").append(values.get(i)).append("\""); - } - sb.append("])"); - return sb.toString(); - } - } - - // Arrays (before the complexType check, to handle Array). - // Nullable items get `.nullable()` to match the TS type (`Array` - // from getTypeDeclaration); top-level nullability of model properties is - // applied externally via x-zod-full. - if (prop.isArray && prop.items != null) { - String item = zodExpr(prop.items); - if (prop.items.isNullable) item += ".nullable()"; - return item + ".array()"; - } - - // Maps / free-form objects with a typed value (additionalProperties). Zod v4 - // requires the two-arg z.record(keyType, valueType) signature. - if (prop.isMap && prop.additionalProperties != null) { - String value = zodExpr(prop.additionalProperties); - if (prop.additionalProperties.isNullable) value += ".nullable()"; - return "z.record(z.string(), " + value + ")"; - } - - // $ref to another model or enum type. - if (prop.complexType != null) { - return camelize(prop.complexType) + "Schema"; - } - - // Primitive types. - if (prop.isInteger || prop.isLong) { - StringBuilder sb = new StringBuilder("z.coerce.number().int()"); - if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); - if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); - return sb.toString(); - } - if (prop.isNumber || prop.isFloat || prop.isDouble) { - StringBuilder sb = new StringBuilder("z.number()"); - if (prop.minimum != null) sb.append(".gte(").append(prop.minimum).append(")"); - if (prop.maximum != null) sb.append(".lte(").append(prop.maximum).append(")"); - return sb.toString(); - } - if (prop.isBoolean) { - return "z.boolean()"; - } - if (prop.isString) { - StringBuilder sb = new StringBuilder("z.string()"); - if (prop.maxLength != null) sb.append(".max(").append(prop.maxLength).append(")"); - if (prop.minLength != null) sb.append(".min(").append(prop.minLength).append(")"); - return sb.toString(); - } - - // Fallback. - return "z.any()"; - } - - // --------------------------------------------------------------------- - // Response / body shape resolution - // --------------------------------------------------------------------- - - /** - * Resolve an operation's response ({@code returnType}) into a Zod schema, wire type, - * and mapper. {@code dir} is {@code TO_WIRE} on the server (domain result -> wire) - * and {@code FROM_WIRE} on the client (wire json -> domain). - */ - static Resolved resolveResponse(CodegenOperation op, Direction dir) { - CodegenProperty rp = op.returnProperty; - String zod = zodExpr(rp); - if (rp == null) { - // DefaultCodegen assigns returnType and returnProperty together, so this is - // unreachable when callers guard on returnType != null; identity as a defensive - // fallback rather than fabricating a "Dto" symbol. - return new Resolved(zod, op.returnType, op.returnType, null); - } - Shape shape = shapeOf(rp, dir, 0); - return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); - } - - /** - * Resolve a request body parameter into a wire type and mapper. {@code zod} is the - * body's Zod expression (built by the caller's parameter builder). {@code dir} is - * {@code FROM_WIRE} on the server (req.body -> domain) and {@code TO_WIRE} on the - * client (domain -> wire before send). - */ - static Resolved resolveBody(CodegenParameter body, Direction dir, String zod) { - Shape shape; - if (body.isArray && body.items != null) { - shape = arrayShape(body.items, dir, 0); - } else if (body.isMap && body.additionalProperties != null) { - shape = mapShape(body.additionalProperties, dir, 0); - } else if (body.isModel) { - // For a scalar model body, CodegenParameter.dataType is the classname of the - // referenced model (e.g. "Pet"). Bodies that dereference to a primitive (e.g. - // a $ref'd enum handled as string by fromRequestBody) have isModel=false. - shape = new Shape("dtos." + body.dataType + "Dto", "types." + body.dataType, - "mappers." + mapPrefix(dir) + body.dataType + "Dto"); - } else { - // Bare primitive: domain == wire, identity mapping. - shape = new Shape(body.dataType, body.dataType, null); - } - return new Resolved(zod, shape.dtoType, shape.domainType, shape.mapper); - } - - /** Wire type, domain type, and mapper (null => identity) for a resolved shape. */ - private static final class Shape { - final String dtoType; - final String domainType; - final String mapper; - - Shape(String dtoType, String domainType, String mapper) { - this.dtoType = dtoType; - this.domainType = domainType; - this.mapper = mapper; - } - } - - /** - * Recursively resolve a property's shape. Containers recurse into their element/value - * type (so nested shapes like {@code Array>} or map-of-array compose - * correctly); {@code depth} suffixes lambda parameters to avoid shadowing. - * - *

Note: container properties carry the innermost ref in {@code complexType} - * (DefaultCodegen.updatePropertyForArray/Map), so isArray/isMap MUST be checked - * before the complexType leaf branches. - */ - private static Shape shapeOf(CodegenProperty p, Direction dir, int depth) { - if (p.isArray && p.items != null) { - return arrayShape(p.items, dir, depth); - } - if (p.isMap && p.additionalProperties != null) { - return mapShape(p.additionalProperties, dir, depth); - } - if (p.complexType != null && !p.isEnum && !p.isEnumRef) { - // Object model — has a generated Dto + mappers. $ref'd enums set isEnumRef - // (not isEnum) and get neither, so they must not take this branch. - return new Shape("dtos." + p.complexType + "Dto", "types." + p.complexType, - "mappers." + mapPrefix(dir) + p.complexType + "Dto"); - } - if (p.complexType != null) { - // Enum (inline via isEnum or $ref via isEnumRef) — domain == wire, identity. - return new Shape("types." + p.complexType, "types." + p.complexType, null); - } - // Primitive or free-form object — domain == wire, identity. - return new Shape(p.dataType, p.dataType, null); - } - - private static Shape arrayShape(CodegenProperty items, Direction dir, int depth) { - Shape inner = shapeOf(items, dir, depth + 1); - String dtoElem = inner.dtoType; - String domainElem = inner.domainType; - String elemMapper = inner.mapper; - if (items.isNullable) { - // Match getTypeDeclaration's Array and skip nulls when mapping. - dtoElem = "(" + dtoElem + " | null)"; - domainElem = "(" + domainElem + " | null)"; - if (elemMapper != null) { - elemMapper = nullSafe(elemMapper, depth); - } - } - String mapper = elemMapper == null - ? null - : "(xs" + depth + ") => xs" + depth + ".map(" + elemMapper + ")"; - return new Shape(dtoElem + "[]", domainElem + "[]", mapper); - } - - private static Shape mapShape(CodegenProperty values, Direction dir, int depth) { - Shape inner = shapeOf(values, dir, depth + 1); - String dtoVal = inner.dtoType; - String domainVal = inner.domainType; - String valMapper = inner.mapper; - if (values.isNullable) { - dtoVal = dtoVal + " | null"; - domainVal = domainVal + " | null"; - if (valMapper != null) { - valMapper = nullSafe(valMapper, depth); - } - } - String mapper = valMapper == null - ? null - : "(m" + depth + ") => Object.fromEntries(Object.entries(m" + depth - + ").map(([k" + depth + ", v" + depth + "]) => [k" + depth - + ", (" + valMapper + ")(v" + depth + ")]))"; - return new Shape("{ [key: string]: " + dtoVal + " }", - "{ [key: string]: " + domainVal + " }", mapper); - } - - /** Wrap a mapper so null/undefined elements pass through untouched. */ - private static String nullSafe(String mapper, int depth) { - String e = "e" + depth; - return "(" + e + ") => " + e + " == null ? " + e + " : (" + mapper + ")(" + e + ")"; - } - - private static String mapPrefix(Direction dir) { - return dir == Direction.TO_WIRE ? "mapTo" : "mapFrom"; - } -} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 105f2274f76e..9fa6ec5de1d1 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -155,6 +155,7 @@ org.openapitools.codegen.languages.TypeScriptClientCodegen org.openapitools.codegen.languages.TypeScriptAngularClientCodegen org.openapitools.codegen.languages.TypeScriptAureliaClientCodegen org.openapitools.codegen.languages.TypeScriptAxiosClientCodegen +org.openapitools.codegen.languages.TypeScriptExpressZodServerCodegen org.openapitools.codegen.languages.TypeScriptFetchClientCodegen org.openapitools.codegen.languages.TypeScriptInversifyClientCodegen org.openapitools.codegen.languages.TypeScriptJqueryClientCodegen @@ -166,6 +167,3 @@ org.openapitools.codegen.languages.TypeScriptRxjsClientCodegen org.openapitools.codegen.languages.WsdlSchemaCodegen org.openapitools.codegen.languages.XojoClientCodegen org.openapitools.codegen.languages.ZapierClientCodegen - -org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen -org.openapitools.codegen.languages.TypescriptExpressZodClientCodegen diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache deleted file mode 100644 index 8b137891791f..000000000000 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/api.mustache +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache b/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache deleted file mode 100644 index 8b137891791f..000000000000 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/model.mustache +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/index.mustache similarity index 65% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/index.mustache index d3d8b3f9bc71..be223c7d1e00 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/index.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/index.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/mappers.mustache similarity index 98% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/mappers.mustache index 807252a5e569..f867c75e4494 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/mappers.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/mappers.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/types.mustache similarity index 98% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/types.mustache index 0d533fb7e2be..9210dc2eb574 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/dtos/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/dtos/types.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/http-client.mustache similarity index 99% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/http-client.mustache index 2c7176282dee..7cc658a75e79 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/http-client.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/http-client.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/index.mustache similarity index 71% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/index.mustache index 6c2070a43e85..f4e733094774 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/index.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/index.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/schemas.mustache similarity index 95% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/schemas.mustache index 2ed4016f9fc7..598183d9ea69 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/schemas.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/schemas.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache b/modules/openapi-generator/src/main/resources/typescript/express-zod/types.mustache similarity index 97% rename from modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache rename to modules/openapi-generator/src/main/resources/typescript/express-zod/types.mustache index 50cb4bc67d02..022c12b90236 100644 --- a/modules/openapi-generator/src/main/resources/typescript-express-zod-client/types.mustache +++ b/modules/openapi-generator/src/main/resources/typescript/express-zod/types.mustache @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptExpressZodServerCodegenOptionsProvider.java similarity index 54% rename from modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java rename to modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptExpressZodServerCodegenOptionsProvider.java index 2a13fcd6b01c..e8a6247cb992 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypescriptExpressZodServerCodegenOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptExpressZodServerCodegenOptionsProvider.java @@ -1,13 +1,10 @@ package org.openapitools.codegen.options; -import org.openapitools.codegen.CodegenConstants; -import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; - import com.google.common.collect.ImmutableMap; import java.util.Map; -public class TypescriptExpressZodServerCodegenOptionsProvider implements OptionsProvider { +public class TypeScriptExpressZodServerCodegenOptionsProvider implements TypeScriptSharedClientOptionsProvider { public static final String PROJECT_NAME_VALUE = "OpenAPI"; @Override @@ -17,8 +14,9 @@ public String getLanguage() { @Override public Map createOptions() { - ImmutableMap.Builder builder = new ImmutableMap.Builder(); - return builder.build(); + return ImmutableMap.builder() + .putAll(TypeScriptSharedClientOptionsProvider.super.createOptions()) + .build(); } @Override diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodClientFrameworkTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodClientFrameworkTest.java new file mode 100644 index 000000000000..af07b361015f --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodClientFrameworkTest.java @@ -0,0 +1,95 @@ +package org.openapitools.codegen.typescript.express.zod; + +import org.openapitools.codegen.ClientOptInput; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.TestUtils; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.languages.TypeScriptClientCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Tests for the {@code express-zod} framework of the unified {@code typescript} generator + * (the client counterpart of {@code typescript-express-zod-server}). + */ +public class TypeScriptExpressZodClientFrameworkTest { + + @Test(description = "express-zod framework replaces the default template set with its supporting files") + public void processOptsRegistersExpressZodFiles() { + final TypeScriptClientCodegen codegen = new TypeScriptClientCodegen(); + codegen.additionalProperties().put("framework", "express-zod"); + codegen.processOpts(); + + Set outputs = codegen.supportingFiles().stream() + .map(sf -> sf.getFolder().isEmpty() + ? sf.getDestinationFilename() + : sf.getFolder() + "/" + sf.getDestinationFilename()) + .collect(Collectors.toSet()); + Assert.assertEquals(outputs, Set.of( + "types.ts", "schemas.ts", "http-client.ts", "index.ts", + "dtos/types.ts", "dtos/mappers.ts", "dtos/index.ts"), + "unexpected supporting files: " + outputs); + Assert.assertTrue(codegen.modelTemplateFiles().isEmpty()); + Assert.assertTrue(codegen.apiTemplateFiles().isEmpty()); + Assert.assertTrue(codegen.apiDocTemplateFiles().isEmpty()); + } + + @Test(description = "express-zod framework generates the express-zod client layout end to end") + public void endToEndGeneration() throws Exception { + final File output = Files.createTempDirectory("typescriptexpresszodclient_").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("typescript") + .addAdditionalProperty("framework", "express-zod") + .setInputSpec("src/test/resources/3_0/typescript-express-zod/petstore.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(clientOptInput).generate(); + files.forEach(File::deleteOnExit); + + for (String expected : new String[]{ + "types.ts", "schemas.ts", "http-client.ts", "index.ts", + "dtos/types.ts", "dtos/mappers.ts", "dtos/index.ts"}) { + Assert.assertTrue(new File(output, expected).exists(), "missing " + expected); + } + for (String unexpected : new String[]{"apis", "models", "package.json", "http"}) { + Assert.assertFalse(new File(output, unexpected).exists(), "unexpected " + unexpected); + } + + // Response schemas resolved via TypeScriptExpressZodSupport (guard regression) + TestUtils.assertFileContains(Paths.get(output + "/schemas.ts"), "ResponseSchema"); + TestUtils.assertFileContains(Paths.get(output + "/http-client.ts"), + "generated by openapi-generator (typescript, framework 'express-zod')"); + } + + @Test(description = "the default fetch-api framework is unaffected by the express-zod hooks") + public void defaultFrameworkStillGeneratesFetchApi() throws Exception { + final File output = Files.createTempDirectory("typescriptdefaultframework_").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("typescript") + .setInputSpec("src/test/resources/3_0/typescript-express-zod/petstore.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(clientOptInput).generate(); + files.forEach(File::deleteOnExit); + + Assert.assertTrue(new File(output, "http/isomorphic-fetch.ts").exists(), + "default framework should still emit the fetch-api http lib"); + Assert.assertTrue(new File(output, "package.json").exists()); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenModelTest.java similarity index 90% rename from modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java rename to modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenModelTest.java index 6c8687ea8c21..db3ea8a82a56 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenModelTest.java @@ -10,13 +10,13 @@ import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.DefaultCodegen; import org.openapitools.codegen.TestUtils; -import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; +import org.openapitools.codegen.languages.TypeScriptExpressZodServerCodegen; import org.testng.Assert; import org.testng.annotations.Test; @SuppressWarnings("static-method") -public class TypescriptExpressZodServerCodegenModelTest { +public class TypeScriptExpressZodServerCodegenModelTest { @Test(description = "convert a simple TypeScript Express Zod model") public void simpleModelTest() { @@ -26,7 +26,7 @@ public void simpleModelTest() { .addProperties("name", new StringSchema()) .addRequiredItem("id") .addRequiredItem("name"); - final DefaultCodegen codegen = new TypescriptExpressZodServerCodegen(); + final DefaultCodegen codegen = new TypeScriptExpressZodServerCodegen(); final OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("Sample", schema); codegen.setOpenAPI(openAPI); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenOptionsTest.java similarity index 58% rename from modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java rename to modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenOptionsTest.java index 4658f4f4ec90..e4ecd5e98354 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenOptionsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenOptionsTest.java @@ -2,17 +2,17 @@ import org.openapitools.codegen.AbstractOptionsTest; import org.openapitools.codegen.CodegenConfig; -import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; -import org.openapitools.codegen.options.TypescriptExpressZodServerCodegenOptionsProvider; +import org.openapitools.codegen.languages.TypeScriptExpressZodServerCodegen; +import org.openapitools.codegen.options.TypeScriptExpressZodServerCodegenOptionsProvider; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -public class TypescriptExpressZodServerCodegenOptionsTest extends AbstractOptionsTest { - private TypescriptExpressZodServerCodegen codegen = mock(TypescriptExpressZodServerCodegen.class, mockSettings); +public class TypeScriptExpressZodServerCodegenOptionsTest extends AbstractOptionsTest { + private TypeScriptExpressZodServerCodegen codegen = mock(TypeScriptExpressZodServerCodegen.class, mockSettings); - public TypescriptExpressZodServerCodegenOptionsTest() { - super(new TypescriptExpressZodServerCodegenOptionsProvider()); + public TypeScriptExpressZodServerCodegenOptionsTest() { + super(new TypeScriptExpressZodServerCodegenOptionsProvider()); } @Override diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenTest.java new file mode 100644 index 000000000000..0935577ad6ee --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypeScriptExpressZodServerCodegenTest.java @@ -0,0 +1,40 @@ +package org.openapitools.codegen.typescript.express.zod; + +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.languages.TypeScriptExpressZodServerCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Set; +import java.util.stream.Collectors; + +public class TypeScriptExpressZodServerCodegenTest { + + @Test(description = "exposes the expected generator metadata") + public void generatorMetadata() { + final TypeScriptExpressZodServerCodegen codegen = new TypeScriptExpressZodServerCodegen(); + Assert.assertEquals(codegen.getName(), "typescript-express-zod-server"); + Assert.assertEquals(codegen.getTag(), CodegenType.SERVER); + } + + @Test(description = "emits everything as supporting files, no per-model/per-api templates") + public void processOptsRegistersSupportingFiles() { + final TypeScriptExpressZodServerCodegen codegen = new TypeScriptExpressZodServerCodegen(); + codegen.processOpts(); + + Set outputs = codegen.supportingFiles().stream() + .map(sf -> sf.getFolder().isEmpty() + ? sf.getDestinationFilename() + : sf.getFolder() + "/" + sf.getDestinationFilename()) + .collect(Collectors.toSet()); + Assert.assertEquals(outputs, Set.of( + "types.ts", "schemas.ts", "index.ts", + "dtos/types.ts", "dtos/mappers.ts", "dtos/index.ts", + "express/handlers.ts", "express/types.ts", "express/router-factory.ts", + "express/errors.ts", "express/auth-middleware.ts", "express/index.ts"), + "unexpected supporting files: " + outputs); + Assert.assertTrue(codegen.modelTemplateFiles().isEmpty()); + Assert.assertTrue(codegen.apiTemplateFiles().isEmpty()); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java deleted file mode 100644 index 8e487719cb48..000000000000 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/express/zod/TypescriptExpressZodServerCodegenTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.openapitools.codegen.typescript.express.zod; - -import org.openapitools.codegen.*; -import org.openapitools.codegen.languages.TypescriptExpressZodServerCodegen; -import io.swagger.models.*; -import io.swagger.parser.SwaggerParser; -import org.testng.Assert; -import org.testng.annotations.Test; - -public class TypescriptExpressZodServerCodegenTest { - - TypescriptExpressZodServerCodegen codegen = new TypescriptExpressZodServerCodegen(); - - @Test - public void shouldSucceed() throws Exception { - // TODO: Complete this test. - Assert.fail("Not implemented."); - } -} diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-express-zod.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-express-zod/petstore.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/3_0/typescript-express-zod.yaml rename to modules/openapi-generator/src/test/resources/3_0/typescript-express-zod/petstore.yaml From f8bafb11f999124df8858558905b5a7184aeea9c Mon Sep 17 00:00:00 2001 From: Istvan Fedak Date: Sat, 11 Jul 2026 14:06:19 -0400 Subject: [PATCH 15/15] Regenerate express-zod client/server samples Regenerated output for the unified typescript generator migration and spec relocation to typescript-express-zod/petstore.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NpXMTYcZS92CKpMmtsjSTp --- .../builds/default/.openapi-generator/FILES | 1 - .../builds/default/dtos/index.ts | 2 +- .../builds/default/dtos/mappers.ts | 2 +- .../builds/default/dtos/types.ts | 2 +- .../builds/default/http-client.ts | 16 ++++++------- .../builds/default/index.ts | 2 +- .../builds/default/schemas.ts | 12 +++++++++- .../builds/default/types.ts | 2 +- .../builds/default/.openapi-generator/FILES | 1 - .../builds/default/express/handlers.ts | 24 +++++++++---------- .../builds/default/schemas.ts | 10 ++++++++ 11 files changed, 46 insertions(+), 28 deletions(-) diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/.openapi-generator/FILES b/samples/client/petstore/typescript-express-zod-client/builds/default/.openapi-generator/FILES index 74f182475dd0..26785f1d5cd0 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/.openapi-generator/FILES +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/.openapi-generator/FILES @@ -1,4 +1,3 @@ -.openapi-generator-ignore dtos/index.ts dtos/mappers.ts dtos/types.ts diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/index.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/index.ts index d3d8b3f9bc71..be223c7d1e00 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/index.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/index.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/mappers.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/mappers.ts index 5b7276381794..b85e4523e58e 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/mappers.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/mappers.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/types.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/types.ts index bee3fc47d507..0c75df04cf5f 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/types.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/dtos/types.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts index 44095eb222b2..5fa42c3ad50c 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/http-client.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. @@ -63,10 +63,10 @@ export class HttpAuthorsService implements types.AuthorsService { const response = await this.fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(params.author), + body: JSON.stringify((mappers.mapToAuthorDto)(params.author)), }); const json = await response.json(); - return mappers.mapFromAuthorDto(schemas.AuthorSchema.parse(json)); + return (mappers.mapFromAuthorDto)(schemas.CreateAuthorResponseSchema.parse(json)); } catch (err) { if (err instanceof ZodError) { return { errors: this.mapZodError(err) } as unknown as types.Author; @@ -81,7 +81,7 @@ export class HttpAuthorsService implements types.AuthorsService { try { const response = await this.fetch(url); const json = await response.json(); - return mappers.mapFromAuthorDto(schemas.AuthorSchema.parse(json)); + return (mappers.mapFromAuthorDto)(schemas.GetAuthorResponseSchema.parse(json)); } catch (err) { if (err instanceof ZodError) { return { errors: this.mapZodError(err) } as unknown as types.Author; @@ -113,10 +113,10 @@ export class HttpCatalogService implements types.CatalogService { const response = await this.fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(params.createBookRequest), + body: JSON.stringify((mappers.mapToCreateBookRequestDto)(params.createBookRequest)), }); const json = await response.json(); - return mappers.mapFromBookDto(schemas.BookSchema.parse(json)); + return (mappers.mapFromBookDto)(schemas.CreateBookResponseSchema.parse(json)); } catch (err) { if (err instanceof ZodError) { return { errors: this.mapZodError(err) } as unknown as types.Book; @@ -150,7 +150,7 @@ export class HttpCatalogService implements types.CatalogService { try { const response = await this.fetch(url); const json = await response.json(); - return mappers.mapFromBookDto(schemas.BookSchema.parse(json)); + return (mappers.mapFromBookDto)(schemas.GetBookResponseSchema.parse(json)); } catch (err) { if (err instanceof ZodError) { return { errors: this.mapZodError(err) } as unknown as types.Book; @@ -165,7 +165,7 @@ export class HttpCatalogService implements types.CatalogService { try { const response = await this.fetch(url); const json = await response.json(); - return mappers.mapFromBookEventDto(schemas.BookEventSchema.parse(json)); + return (mappers.mapFromBookEventDto)(schemas.GetLatestBookEventResponseSchema.parse(json)); } catch (err) { if (err instanceof ZodError) { return { errors: this.mapZodError(err) } as unknown as types.BookEvent; diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/index.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/index.ts index 6c2070a43e85..f4e733094774 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/index.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/index.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/schemas.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/schemas.ts index 46c6c23f5418..54043b987553 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/schemas.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/schemas.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. @@ -48,3 +48,13 @@ export const ModelErrorSchema = z.object({ code: z.string().optional(), }); +export const CreateAuthorResponseSchema = AuthorSchema; + +export const CreateBookResponseSchema = BookSchema; + +export const GetAuthorResponseSchema = AuthorSchema; + +export const GetBookResponseSchema = BookSchema; + +export const GetLatestBookEventResponseSchema = BookEventSchema; + diff --git a/samples/client/petstore/typescript-express-zod-client/builds/default/types.ts b/samples/client/petstore/typescript-express-zod-client/builds/default/types.ts index fed7cd1c6a22..ac3004fbcf9c 100644 --- a/samples/client/petstore/typescript-express-zod-client/builds/default/types.ts +++ b/samples/client/petstore/typescript-express-zod-client/builds/default/types.ts @@ -1,5 +1,5 @@ /** - * This code was generated by openapi-generator (typescript-express-zod-client). + * This code was generated by openapi-generator (typescript, framework 'express-zod'). * * Changes to this file may cause incorrect behavior and will be lost if * the code is regenerated. diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/.openapi-generator/FILES b/samples/server/petstore/typescript-express-zod-server/builds/default/.openapi-generator/FILES index 06566e21da80..0184b27bbaeb 100644 --- a/samples/server/petstore/typescript-express-zod-server/builds/default/.openapi-generator/FILES +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/.openapi-generator/FILES @@ -1,4 +1,3 @@ -.openapi-generator-ignore dtos/index.ts dtos/mappers.ts dtos/types.ts diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts index 267db1dee9f9..db3f47cb7e31 100644 --- a/samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/express/handlers.ts @@ -25,7 +25,7 @@ export const handleCreateAuthor = // Parse parameters from request const params: types.CreateAuthorParams = schemas.CreateAuthorParamsSchema.parse({ - author: mappers.mapFromAuthorDto(req.body), + author: ((b) => b == null ? b : (mappers.mapFromAuthorDto)(b))(req.body), }); // Execute service method @@ -39,8 +39,8 @@ export const handleCreateAuthor = res, next, status, - mappers.mapToAuthorDto(result), - (payload) => schemas.AuthorSchema.parse(payload), + (mappers.mapToAuthorDto)(result), + (payload) => schemas.CreateAuthorResponseSchema.parse(payload), ); } } catch (err) { @@ -63,7 +63,7 @@ export const handleCreateBook = // Parse parameters from request const params: types.CreateBookParams = schemas.CreateBookParamsSchema.parse({ - createBookRequest: mappers.mapFromCreateBookRequestDto(req.body), + createBookRequest: ((b) => b == null ? b : (mappers.mapFromCreateBookRequestDto)(b))(req.body), }); // Execute service method @@ -77,8 +77,8 @@ export const handleCreateBook = res, next, status, - mappers.mapToBookDto(result), - (payload) => schemas.BookSchema.parse(payload), + (mappers.mapToBookDto)(result), + (payload) => schemas.CreateBookResponseSchema.parse(payload), ); } } catch (err) { @@ -152,8 +152,8 @@ export const handleGetAuthor = res, next, status, - mappers.mapToAuthorDto(result), - (payload) => schemas.AuthorSchema.parse(payload), + (mappers.mapToAuthorDto)(result), + (payload) => schemas.GetAuthorResponseSchema.parse(payload), ); } } catch (err) { @@ -191,8 +191,8 @@ export const handleGetBook = res, next, status, - mappers.mapToBookDto(result), - (payload) => schemas.BookSchema.parse(payload), + (mappers.mapToBookDto)(result), + (payload) => schemas.GetBookResponseSchema.parse(payload), ); } } catch (err) { @@ -229,8 +229,8 @@ export const handleGetLatestBookEvent = res, next, status, - mappers.mapToBookEventDto(result), - (payload) => schemas.BookEventSchema.parse(payload), + (mappers.mapToBookEventDto)(result), + (payload) => schemas.GetLatestBookEventResponseSchema.parse(payload), ); } } catch (err) { diff --git a/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts b/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts index f01413694de5..f064b2b98300 100644 --- a/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts +++ b/samples/server/petstore/typescript-express-zod-server/builds/default/schemas.ts @@ -73,3 +73,13 @@ export const GetLatestBookEventParamsSchema = z.object({ bookId: z.coerce.number().int(), }); +export const CreateAuthorResponseSchema = AuthorSchema; + +export const CreateBookResponseSchema = BookSchema; + +export const GetAuthorResponseSchema = AuthorSchema; + +export const GetBookResponseSchema = BookSchema; + +export const GetLatestBookEventResponseSchema = BookEventSchema; +