"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = void 0;
exports.serializeOpenApiSchema = serializeOpenApiSchema;
exports.dumpOpenApiSchema = dumpOpenApiSchema;
exports.serializeOpenApiYaml = serializeOpenApiYaml;
exports.shouldExposeOpenApi = shouldExposeOpenApi;
exports.shouldDumpOpenApiSchema = shouldDumpOpenApiSchema;
const promises_1 = require("node:fs/promises");
const node_http_1 = require("node:http");
const node_path_1 = require("node:path");
const reflection_1 = require("../reflection");
const yaml_1 = require("yaml");
const helpers_1 = require("../helpers");
const env_1 = require("../env");
const http_1 = require("../http");
const decorators_1 = require("./decorators");
const schema_1 = require("./schema");
function serializeOpenApiSchema(app, options = {}) {
    const packageJson = (0, helpers_1.getPackageJson)();
    const schemaContext = (0, schema_1.createOpenApiSchemaContext)();
    const document = {
        openapi: '3.1.0',
        jsonSchemaDialect: 'https://spec.openapis.org/oas/3.1/dialect/base',
        info: {
            title: options.title ?? packageJson?.name ?? 'API',
            version: options.version ?? packageJson?.version ?? '0.0.0'
        },
        paths: {}
    };
    for (const route of app.router.listRoutes()) {
        if (!options.includeInternal && isInternalRoute(route.path))
            continue;
        if ((0, decorators_1.isOpenApiIgnored)(route.controllerClass, route.propertyKey))
            continue;
        const method = toOpenApiMethod(route.method);
        if (!method)
            continue;
        const path = toOpenApiPath(route.path);
        document.paths[path] ??= {};
        document.paths[path][method] = buildOperation(route, schemaContext);
    }
    if (Object.keys(schemaContext.schemas).length) {
        document.components ??= {};
        document.components.schemas = schemaContext.schemas;
    }
    if (usesBearerAuth(document)) {
        document.components ??= {};
        document.components.securitySchemes = {
            ...document.components.securitySchemes,
            bearerAuth: {
                type: 'http',
                scheme: 'bearer',
                bearerFormat: 'JWT'
            }
        };
    }
    return document;
}
async function dumpOpenApiSchema(app, options = {}) {
    const outputPath = options.path ?? (0, node_path_1.join)(process.cwd(), 'openapi.yaml');
    await (0, promises_1.writeFile)(outputPath, serializeOpenApiYaml(app, options));
    return outputPath;
}
function serializeOpenApiYaml(app, options = {}) {
    return (0, yaml_1.stringify)(serializeOpenApiSchema(app, options), {
        aliasDuplicateObjects: false
    });
}
function shouldExposeOpenApi(config) {
    const route = config.ENABLE_OPENAPI_ROUTE;
    if (isTruthyConfigValue(route))
        return true;
    if (isFalseyConfigValue(route))
        return false;
    const legacy = config.ENABLE_OPENAPI_SCHEMA;
    if (isTruthyConfigValue(legacy))
        return true;
    if (isFalseyConfigValue(legacy))
        return false;
    return config.APP_ENV === 'test' || config.APP_ENV === 'development';
}
function shouldDumpOpenApiSchema(config) {
    const legacy = config.ENABLE_OPENAPI_SCHEMA;
    if (isTruthyConfigValue(legacy))
        return true;
    if (isFalseyConfigValue(legacy))
        return false;
    return config.APP_ENV === 'development' && env_1.Env.NODE_TEST_CONTEXT === undefined;
}
function isTruthyConfigValue(value) {
    return value === true || value === 'true' || value === '1';
}
function isFalseyConfigValue(value) {
    return value === false || value === 'false' || value === '0';
}
function buildOperation(route, schemaContext) {
    const parameters = buildParameters(route, schemaContext);
    const requestBody = buildRequestBody(route, schemaContext);
    const tagName = getControllerTagName(route.controllerClass);
    const operation = {
        operationId: getOperationId(route, tagName),
        tags: [tagName],
        responses: buildResponses(route, schemaContext)
    };
    const description = getRouteDescription(route);
    if (description)
        operation.summary = description;
    if (parameters.length)
        operation.parameters = parameters;
    if (requestBody)
        operation.requestBody = requestBody;
    if (route.parameters.some(parameter => parameter.kind === 'jwt' && !parameter.optional)) {
        operation.security = [{ bearerAuth: [] }];
    }
    return operation;
}
function buildParameters(route, schemaContext) {
    const parameters = [];
    const documentedPathParams = new Set();
    for (const parameter of route.parameters) {
        if (parameter.kind === 'path') {
            documentedPathParams.add(parameter.name);
            parameters.push(buildOpenApiParameter('path', parameter, schemaContext));
        }
        else if (parameter.kind === 'query') {
            parameters.push(buildOpenApiParameter('query', parameter, schemaContext));
        }
        else if (parameter.kind === 'header') {
            parameters.push(buildOpenApiParameter('header', parameter, schemaContext));
        }
        else if (parameter.kind === 'queries') {
            parameters.push(...expandObjectParameters('query', parameter, schemaContext));
        }
    }
    for (const name of route.paramNames) {
        if (documentedPathParams.has(name))
            continue;
        parameters.push({
            name,
            in: 'path',
            required: true,
            schema: { type: 'string' }
        });
    }
    return parameters;
}
function buildOpenApiParameter(location, parameter, schemaContext) {
    const schema = (0, schema_1.typeToOpenApiSchema)(parameter.type, schemaContext);
    return {
        name: parameter.name,
        in: location,
        required: location === 'path' ? true : !parameter.optional && !(0, schema_1.allowsUndefined)(parameter.type),
        schema: isEmptySchema(schema) ? { type: 'string' } : schema
    };
}
function expandObjectParameters(location, parameter, schemaContext) {
    const properties = (0, schema_1.listOpenApiTypeProperties)(parameter.type);
    if (!properties.length)
        return [buildOpenApiParameter(location, parameter, schemaContext)];
    (0, schema_1.typeToOpenApiSchema)(parameter.type, schemaContext);
    return properties.map(property => ({
        name: property.name,
        in: location,
        required: !parameter.optional && property.required,
        description: property.description,
        schema: (0, schema_1.typeToOpenApiSchema)(property.type, schemaContext)
    }));
}
function buildRequestBody(route, schemaContext) {
    const bodyParams = route.parameters.filter((parameter) => parameter.kind === 'body');
    const fileParams = route.parameters.filter((parameter) => parameter.kind === 'file');
    const bodyParamsContainFiles = bodyParams.some(parameter => (0, schema_1.typeHasOpenApiFileUpload)(parameter.type));
    const bodyParamsRequireFiles = bodyParams.some(parameter => (0, schema_1.typeRequiresOpenApiFileUpload)(parameter.type));
    if (!bodyParams.length && !fileParams.length)
        return undefined;
    if (!fileParams.length && bodyParams.length === 1 && bodyParamsContainFiles) {
        const encoding = uploadEncodingForRoute(route);
        const schema = (0, schema_1.typeToOpenApiSchema)(bodyParams[0].type, schemaContext);
        const content = {
            'multipart/form-data': {
                schema,
                encoding: Object.keys(encoding).length ? encoding : undefined
            }
        };
        if (!bodyParamsRequireFiles) {
            content['application/json'] = { schema: schemaForJsonBodyWithoutFileUploads(bodyParams[0].type, schemaContext) };
        }
        return {
            required: !bodyParams[0].optional,
            content
        };
    }
    if (fileParams.length) {
        const schema = { type: 'object', properties: {} };
        const required = [];
        const encoding = {};
        for (const file of fileParams) {
            schema.properties[file.name] = openApiFileSchema(route.uploadPolicy.files?.[file.name]);
            const contentType = uploadPolicyContentType(route.uploadPolicy.files?.[file.name]);
            if (contentType)
                encoding[file.name] = { contentType };
            if (!file.optional)
                required.push(file.name);
        }
        if (bodyParams.length) {
            const bodySchema = bodyParams.length === 1 ? (0, schema_1.typeToOpenApiSchema)(bodyParams[0].type, schemaContext) : schemaForMultipleBodies(bodyParams, schemaContext);
            schema.properties._payload = bodySchema;
            encoding._payload = { contentType: 'application/json' };
            if (bodyParams.some(parameter => !parameter.optional))
                required.push('_payload');
        }
        if (required.length)
            schema.required = required;
        return {
            required: required.length > 0,
            content: {
                'multipart/form-data': {
                    schema,
                    encoding: Object.keys(encoding).length ? encoding : undefined
                }
            }
        };
    }
    return {
        required: bodyParams.some(parameter => !parameter.optional),
        content: {
            'application/json': {
                schema: bodyParams.length === 1
                    ? (0, schema_1.typeToOpenApiSchema)(bodyParams[0].type, schemaContext)
                    : schemaForMultipleBodies(bodyParams, schemaContext)
            }
        }
    };
}
function schemaForJsonBodyWithoutFileUploads(type, schemaContext) {
    const schema = { type: 'object', properties: {} };
    const required = [];
    for (const property of (0, schema_1.listOpenApiTypeProperties)(type)) {
        if ((0, schema_1.typeHasOpenApiFileUpload)(property.type))
            continue;
        const propertySchema = (0, schema_1.typeToOpenApiSchema)(property.type, schemaContext);
        if (property.description && !('$ref' in propertySchema))
            propertySchema.description = property.description;
        schema.properties[property.name] = propertySchema;
        if (property.required)
            required.push(property.name);
    }
    if (required.length)
        schema.required = required;
    return schema;
}
function uploadEncodingForRoute(route) {
    const encoding = {};
    for (const [name, policy] of Object.entries(route.uploadPolicy.files ?? {})) {
        const contentType = uploadPolicyContentType(policy);
        if (contentType)
            encoding[name] = { contentType };
    }
    return encoding;
}
function openApiFileSchema(policy) {
    const schema = { type: 'string', format: 'binary' };
    if (policy?.maxSizeBytes !== undefined)
        schema['x-maxSizeBytes'] = policy.maxSizeBytes;
    if (policy?.allowedTypes?.length)
        schema['x-allowedTypes'] = policy.allowedTypes;
    return schema;
}
function uploadPolicyContentType(policy) {
    return policy?.allowedTypes?.length ? policy.allowedTypes.join(', ') : undefined;
}
function schemaForMultipleBodies(bodyParams, schemaContext) {
    const schema = { type: 'object', properties: {} };
    const required = [];
    for (const body of bodyParams) {
        schema.properties[body.name] = (0, schema_1.typeToOpenApiSchema)(body.type, schemaContext);
        if (!body.optional)
            required.push(body.name);
    }
    if (required.length)
        schema.required = required;
    return schema;
}
function buildResponses(route, schemaContext) {
    const responses = buildInferredResponses(route, schemaContext);
    for (const response of (0, decorators_1.getOpenApiResponses)(route.controllerClass, route.propertyKey)) {
        const typedResponse = buildTypedResponse(response.status, response.type, schemaContext, response.description);
        responses[String(response.status)] = typedResponse[String(response.status)];
    }
    return responses;
}
function buildInferredResponses(route, schemaContext) {
    const apiResponse = getApiResponseMetadata(route.returnType);
    if (apiResponse)
        return buildTypedResponse(apiResponse.status, apiResponse.type, schemaContext);
    const returnType = (0, schema_1.unwrapOpenApiType)(route.returnType);
    const typeName = getHttpResponseAliasName(route.returnType) ?? getTypeName(route.returnType) ?? getTypeName(returnType);
    if (typeName === 'RedirectResponse') {
        return {
            '302': {
                description: 'Redirect',
                headers: {
                    location: { schema: { type: 'string' } }
                }
            }
        };
    }
    if (isClassType(returnType, http_1.RedirectResponseResult)) {
        return {
            default: {
                description: 'Redirect',
                headers: {
                    location: { schema: { type: 'string' } }
                }
            }
        };
    }
    if (typeName === 'EmptyResponse') {
        return { '204': { description: 'No Content' } };
    }
    if (isClassType(returnType, http_1.EmptyResponseResult)) {
        return { default: { description: 'No Content' } };
    }
    if (returnType.kind === reflection_1.ReflectionKind.void || returnType.kind === reflection_1.ReflectionKind.undefined) {
        return { '200': { description: 'OK' } };
    }
    if (typeName === 'AnyResponse') {
        return {
            '200': {
                description: 'OK',
                content: {
                    'application/json': {
                        schema: {}
                    }
                }
            }
        };
    }
    if (typeName === 'OkResponse') {
        return {
            '200': {
                description: 'OK',
                content: {
                    'application/json': {
                        schema: {}
                    }
                }
            }
        };
    }
    if (isClassType(returnType, http_1.JsonResponseResult)) {
        return {
            default: {
                description: 'OK',
                content: {
                    'application/json': {
                        schema: {}
                    }
                }
            }
        };
    }
    if (isClassType(returnType, http_1.RawResponseResult)) {
        return {
            default: {
                description: 'OK'
            }
        };
    }
    return {
        '200': {
            description: 'OK',
            content: {
                'application/json': {
                    schema: (0, schema_1.typeToOpenApiSchema)(route.returnType, schemaContext)
                }
            }
        }
    };
}
function buildTypedResponse(status, type, schemaContext, description) {
    const returnType = (0, schema_1.unwrapOpenApiType)(type);
    if (returnType.kind === reflection_1.ReflectionKind.void || returnType.kind === reflection_1.ReflectionKind.undefined) {
        return { [status]: { description: description ?? responseDescription(status) } };
    }
    return {
        [status]: {
            description: description ?? responseDescription(status),
            content: {
                'application/json': {
                    schema: (0, schema_1.typeToOpenApiSchema)(type, schemaContext)
                }
            }
        }
    };
}
function responseDescription(status) {
    return node_http_1.STATUS_CODES[status] ?? 'Response';
}
function getApiResponseMetadata(type) {
    const options = getAnnotationOptions(type, 'openapi:response');
    const bodyType = getAnnotationOptionType(options, 'type') ?? getTypeArgument(type, 0) ?? getPromiseAliasType(type);
    if (bodyType) {
        return {
            status: getAnnotationOptionNumber(options, 'status') ?? literalNumber(getTypeArgument(type, 1)) ?? 200,
            type: bodyType
        };
    }
    if (type.kind === reflection_1.ReflectionKind.intersection || type.kind === reflection_1.ReflectionKind.union) {
        for (const item of type.types) {
            const nested = getApiResponseMetadata(item);
            if (nested)
                return nested;
        }
    }
}
function getPromiseAliasType(type) {
    return getTypeName(type) === 'ApiResponse' && type.kind === reflection_1.ReflectionKind.promise ? type.type : undefined;
}
function getTypeArgument(type, index) {
    return (type.typeArguments?.[index] ??
        type.arguments?.[index]);
}
function getAnnotationOptions(type, annotation) {
    const options = reflection_1.typeAnnotation.getType(type, annotation) ?? reflection_1.typeAnnotation.getOption(type, annotation);
    if (options !== undefined)
        return options;
    if (type.kind === reflection_1.ReflectionKind.intersection || type.kind === reflection_1.ReflectionKind.union) {
        for (const item of type.types) {
            const nested = getAnnotationOptions(item, annotation);
            if (nested !== undefined)
                return nested;
        }
    }
}
function getAnnotationOptionType(options, name) {
    const value = getAnnotationOption(options, name);
    return (0, reflection_1.isReflectedType)(value) ? value : undefined;
}
function getAnnotationOptionNumber(options, name) {
    const value = getAnnotationOption(options, name);
    return typeof value === 'number' ? value : undefined;
}
function getAnnotationOption(options, name) {
    if (!options || typeof options !== 'object')
        return undefined;
    if (name in options && !options.kind)
        return options[name];
    if (options.kind === reflection_1.ReflectionKind.objectLiteral) {
        const property = options.types.find(item => item.kind === reflection_1.ReflectionKind.propertySignature && String(item.name) === name);
        if (property?.kind !== reflection_1.ReflectionKind.propertySignature)
            return undefined;
        if (property.type.kind === reflection_1.ReflectionKind.literal)
            return property.type.literal;
        return property.type;
    }
}
function literalNumber(type) {
    return type?.kind === reflection_1.ReflectionKind.literal && typeof type.literal === 'number' ? type.literal : undefined;
}
function toOpenApiMethod(method) {
    const lower = method.toLowerCase();
    return ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].includes(lower) ? lower : undefined;
}
function toOpenApiPath(path) {
    return path.replace(/:([^/]+)/g, '{$1}');
}
function isInternalRoute(path) {
    return (path === '/openapi.json' ||
        path === '/openapi.yaml' ||
        path === '/healthz' ||
        path === '/readyz' ||
        path === '/livez' ||
        path === '/metrics' ||
        path.startsWith('/_devconsole'));
}
function getControllerTagName(controllerClass) {
    return camelCaseIdentifier(controllerClass.name.replace(/Controller$/, '') || controllerClass.name || 'controller');
}
function getOperationId(route, tagName) {
    return camelCaseIdentifier([route.method.toLowerCase(), tagName, route.methodName]);
}
function camelCaseIdentifier(parts) {
    const words = (Array.isArray(parts) ? parts.join(' ') : parts)
        .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
        .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
        .split(/[^a-zA-Z0-9]+/)
        .filter(Boolean);
    if (!words.length)
        return '';
    return words
        .map((word, index) => {
        const normalized = word.toLowerCase();
        return index === 0 ? normalized : normalized.charAt(0).toUpperCase() + normalized.slice(1);
    })
        .join('');
}
function isRequiredParameterType(type) {
    const unwrapped = (0, schema_1.unwrapOpenApiType)(type);
    return !(unwrapped.kind === reflection_1.ReflectionKind.union && unwrapped.types.some(item => item.kind === reflection_1.ReflectionKind.undefined));
}
function getRouteDescription(route) {
    return route.description;
}
function getTypeName(type) {
    return type.typeName;
}
function getHttpResponseAliasName(type) {
    const typeName = getTypeName(type);
    if (isHttpResponseAliasName(typeName))
        return typeName;
    if (type.kind === reflection_1.ReflectionKind.promise)
        return getHttpResponseAliasName(type.type);
    if (type.kind === reflection_1.ReflectionKind.intersection || type.kind === reflection_1.ReflectionKind.union) {
        for (const item of type.types) {
            const nested = getHttpResponseAliasName(item);
            if (nested)
                return nested;
        }
    }
}
function isHttpResponseAliasName(typeName) {
    return typeName === 'AnyResponse' || typeName === 'OkResponse' || typeName === 'RedirectResponse' || typeName === 'EmptyResponse';
}
function isClassType(type, classType) {
    return type.kind === reflection_1.ReflectionKind.class && type.classType === classType;
}
function usesBearerAuth(document) {
    return Object.values(document.paths).some(path => Object.values(path).some(operation => operation?.security?.some(item => item.bearerAuth)));
}
function isEmptySchema(schema) {
    return !!schema && typeof schema === 'object' && !('$ref' in schema) && Object.keys(schema).length === 0;
}
exports.__tsfTypeAliases = JSON.parse("[1,{\"DumpOpenApiOptions\":{\"kind\":18,\"typeName\":\"DumpOpenApiOptions\",\"types\":[{\"kind\":20,\"name\":\"title\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"version\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"includeInternal\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"path\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]},\"SerializeOpenApiOptions\":{\"kind\":18,\"typeName\":\"SerializeOpenApiOptions\",\"types\":[{\"kind\":20,\"name\":\"title\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"version\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"includeInternal\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true}]}}]")[1];
//# sourceMappingURL=serializer.js.map