@nestia/sdk
Version:
Nestia SDK and Swagger generator
205 lines (200 loc) • 7.12 kB
text/typescript
import { MetadataFactory } from "typia/lib/factories/MetadataFactory";
import { IMetadata } from "typia/lib/schemas/metadata/IMetadata";
import { IMetadataComponents } from "typia/lib/schemas/metadata/IMetadataComponents";
import { IMetadataDictionary } from "typia/lib/schemas/metadata/IMetadataDictionary";
import { Metadata } from "typia/lib/schemas/metadata/Metadata";
import { MetadataComponents } from "typia/lib/schemas/metadata/MetadataComponents";
import { MetadataObjectType } from "typia/lib/schemas/metadata/MetadataObjectType";
import { Escaper } from "typia/lib/utils/Escaper";
import { IReflectController } from "../structures/IReflectController";
import { IReflectHttpOperation } from "../structures/IReflectHttpOperation";
import { IReflectOperationError } from "../structures/IReflectOperationError";
import { ITypedHttpRoute } from "../structures/ITypedHttpRoute";
import { ITypedHttpRouteException } from "../structures/ITypedHttpRouteException";
import { ITypedHttpRouteParameter } from "../structures/ITypedHttpRouteParameter";
import { ITypedHttpRouteSuccess } from "../structures/ITypedHttpRouteSuccess";
import { PathUtil } from "../utils/PathUtil";
export namespace TypedHttpRouteAnalyzer {
export const dictionary = (
controllers: IReflectController[],
): IMetadataDictionary => {
const individual: IMetadataComponents[] = [];
for (const c of controllers)
for (const o of c.operations) {
if (o.protocol !== "http") continue;
if (o.success) individual.push(o.success.components);
for (const p of o.parameters) individual.push(p.components);
for (const e of Object.values(o.exceptions))
individual.push(e.components);
}
const components: MetadataComponents = MetadataComponents.from({
objects: Object.values(
Object.fromEntries(
individual.map((c) => c.objects.map((o) => [o.name, o])).flat(),
),
),
arrays: Object.values(
Object.fromEntries(
individual.map((c) => c.arrays.map((a) => [a.name, a])).flat(),
),
),
tuples: Object.values(
Object.fromEntries(
individual.map((c) => c.tuples.map((t) => [t.name, t])).flat(),
),
),
aliases: Object.values(
Object.fromEntries(
individual.map((c) => c.aliases.map((a) => [a.name, a])).flat(),
),
),
});
return components.dictionary;
};
export const analyze = (props: {
controller: IReflectController;
errors: IReflectOperationError[];
dictionary: IMetadataDictionary;
operation: IReflectHttpOperation;
paths: string[];
}): ITypedHttpRoute[] => {
const errors: IReflectOperationError[] = [];
const cast = (
next: {
metadata: IMetadata;
validate: MetadataFactory.Validator;
},
from: string,
escape: boolean,
): Metadata => {
const metadata: Metadata = Metadata.from(next.metadata, props.dictionary);
const metaErrors: MetadataFactory.IError[] = MetadataFactory.validate({
options: {
escape,
constant: true,
absorb: true,
validate: next.validate, // @todo -> CHECK IN TYPIA
},
functor: next.validate, // @todo -> CHECK IN TYPIA
metadata,
});
if (metaErrors.length)
errors.push({
file: props.controller.file,
class: props.controller.class.name,
function: props.operation.name,
from,
contents: metaErrors.map((e) => ({
name: e.name,
accessor:
e.explore.object !== null
? join({
object: e.explore.object,
key: e.explore.property,
})
: null,
messages: e.messages,
})),
});
return metadata;
};
const exceptions: Record<
number | "2XX" | "3XX" | "4XX" | "5XX",
ITypedHttpRouteException
> = Object.fromEntries(
Object.entries(props.operation.exceptions).map(([key, value]) => [
key as any,
{
status: value.status,
description: value.description,
example: value.example,
examples: value.examples,
type: value.type,
metadata: cast(value, `exception (status: ${key})`, true),
},
]),
);
const parameters: ITypedHttpRouteParameter[] =
props.operation.parameters.map((p) => ({
...p,
metadata: cast(
p,
`parameter (name: ${JSON.stringify(p.name)})`,
p.category === "body" &&
(p.contentType === "application/json" || p.encrypted === true),
),
}));
const success: ITypedHttpRouteSuccess = {
...props.operation.success,
metadata: cast(
props.operation.success,
"success",
props.operation.success.encrypted ||
props.operation.success.contentType === "application/json",
),
setHeaders: props.operation.jsDocTags
.filter(
(t) =>
t.text?.length &&
t.text[0].text &&
(t.name === "setHeader" || t.name === "assignHeaders"),
)
.map((t) =>
t.name === "setHeader"
? {
type: "setter",
source: t.text![0].text.split(" ")[0].trim(),
target: t.text![0].text.split(" ")[1]?.trim(),
}
: {
type: "assigner",
source: t.text![0].text,
},
),
};
if (errors.length) {
props.errors.push(...errors);
return [];
}
return props.paths.map(
(path) =>
({
...props.operation,
controller: props.controller,
path,
accessor: [...PathUtil.accessors(path), props.operation.name],
exceptions,
pathParameters: parameters.filter((p) => p.category === "param"),
queryParameters: parameters
.filter((p) => p.category === "query")
.filter((p) => p.field !== null),
headerParameters: parameters
.filter((p) => p.category === "headers")
.filter((p) => p.field !== null),
queryObject:
parameters
.filter((p) => p.category === "query")
.filter((p) => p.field === null)[0] ?? null,
body: parameters.filter((p) => p.category === "body")[0] ?? null,
headerObject:
parameters
.filter((p) => p.category === "headers")
.filter((p) => p.field === null)[0] ?? null,
success,
extensions: props.operation.extensions,
}) satisfies ITypedHttpRoute,
);
};
}
const join = ({
object,
key,
}: {
object: MetadataObjectType;
key: string | object | null;
}) => {
if (key === null) return object.name;
else if (typeof key === "object") return `${object.name}[key]`;
else if (Escaper.variable(key)) return `${object.name}.${key}`;
return `${object.name}[${JSON.stringify(key)}]`;
};