Skip to content

Commit

Permalink
lots of bug fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jfrconley committed Dec 17, 2023
1 parent 6501416 commit 57c216d
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 386 deletions.
26 changes: 23 additions & 3 deletions packages/rest/src/runtime/decorators.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Nornir} from "@nornir/core";
import {HttpRequest, HttpResponse} from "./http-event.mjs";
import {HttpRequest, HttpResponse, HttpStatusCode, MimeType} from "./http-event.mjs";
import {InstanceOf} from "ts-morph";

const UNTRANSFORMED_ERROR = new Error("nornir/rest decorators have not been transformed. Have you setup ts-patch/ttypescript and added the originator to your tsconfig.json?");
Expand All @@ -15,11 +15,31 @@ export function Controller<const Path extends string, const ApiId extends string
};
}

const routeChainDecorator = <Input extends HttpRequest, Output extends HttpResponse>(
_target: (chain: Nornir<Input>) => Nornir<Input, Output>,
const routeChainDecorator = <Input extends HttpRequest, Output extends HttpResponse >(
_target: (chain: Nornir<ValidateRequestType<Input>>) => Nornir<ValidateRequestType<Input>, ValidateResponseType<Output>>,
_propertyKey: ClassMethodDecoratorContext,
): never => {throw UNTRANSFORMED_ERROR};

export type ValidateRequestType<T extends HttpRequest> = RequestResponseWithBodyHasContentType<T> extends true ? T : "Request type with a body must have a content-type header";
export type ValidateResponseType<T extends HttpResponse> = RequestResponseWithBodyHasContentType<T> extends true ?
OutputHasSpecifiedStatusCode<T> extends true
? T : "Response type must have a status code specified" : "Response type with a body must have a content-type header";

type OutputHasSpecifiedStatusCode<Output extends HttpResponse> = IfEquals<Output["statusCode"], HttpStatusCode, false, true>;

type RequestResponseWithBodyHasContentType<T extends HttpResponse | HttpRequest> =
// No body spec is valid
HasBody<T> extends false ? true :
// Empty body is valid
T extends { body?: undefined | null } ? true :
T["headers"]["content-type"] extends string ?
IfEquals<T["headers"]["content-type"], MimeType | undefined, false, true>
: false;

type HasBody<T extends HttpResponse | HttpRequest> = T extends { body: any } ? true : false

Check failure on line 39 in packages/rest/src/runtime/decorators.mts

View workflow job for this annotation

GitHub Actions / Lint Back-end code

Unexpected any. Specify a different type

type Test = { statusCode: HttpStatusCode.Ok, headers: NonNullable<unknown>, body: string}

Check warning on line 41 in packages/rest/src/runtime/decorators.mts

View workflow job for this annotation

GitHub Actions / Lint Back-end code

'Test' is defined but never used. Allowed unused vars must match /^_/u

/**
* Use to mark a method as a GET route
*
Expand Down
71 changes: 10 additions & 61 deletions packages/rest/src/runtime/http-event.mts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface HttpResponseEmpty extends HttpResponse {
readonly body?: undefined
}

/**
* @ignore
*/
export enum HttpStatusCode {
Continue = "100",
SwitchingProtocols = "101",
Expand Down Expand Up @@ -123,69 +126,15 @@ export enum HttpStatusCode {
LoopDetected = "508",
NotExtended = "510",
}
//
// export type HttpStatusCode =
// | "100"
// | "101"
// | "102"
// | "200"
// | "201"
// | "202"
// | "203"
// | "204"
// | "205"
// | "206"
// | "207"
// | "208"
// | "226"
// | "300"
// | "301"
// | "302"
// | "303"
// | "304"
// | "305"
// | "307"
// | "308"
// | "400"
// | "401"
// | "402"
// | "403"
// | "404"
// | "405"
// | "406"
// | "407"
// | "408"
// | "409"
// | "410"
// | "411"
// | "412"
// | "413"
// | "414"
// | "415"
// | "416"
// | "417"
// | "418"
// | "421"
// | "422"
// | "423"
// | "424"
// | "426"
// | "428"
// | "429"
// | "431"
// | "451"
// | "500"
// | "501"
// | "502"
// | "503"
// | "504"
// | "505"
// | "506"
// | "507"
// | "508"
// | "510";

/**
* @ignore
*/
export enum MimeType {
/**
* @ignore
*/
None = "",
ApplicationJson = "application/json",
ApplicationOctetStream = "application/octet-stream",
ApplicationPdf = "application/pdf",
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/runtime/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {httpErrorHandler} from "./error.mjs";

export {
GetChain, Controller, PostChain, DeleteChain, HeadChain, OptionsChain, PatchChain, PutChain,
Provider
Provider, ValidateRequestType, ValidateResponseType
} from './decorators.mjs';
export {
HttpResponse, HttpRequest, HttpEvent, HttpMethod, HttpRequestEmpty, HttpResponseEmpty,
Expand Down
12 changes: 6 additions & 6 deletions packages/rest/src/runtime/serialize.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import {HttpResponse, MimeType, SerializedHttpResponse} from "./http-event.mjs";

import {getContentType} from "./utils.mjs";

export type HttpBodySerializer<T> = (body: T | undefined) => Buffer
export type HttpBodySerializer = (body: unknown | undefined) => Buffer

export type HttpBodySerializerMap = Partial<Record<MimeType | "default", HttpBodySerializer<never>>>
export type HttpBodySerializerMap = Partial<Record<MimeType | "default", HttpBodySerializer>>

const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer<never>} = {
"application/json": (body?: object) => Buffer.from(JSON.stringify(body) || "", "utf8"),
"text/plain": (body?: string) => Buffer.from(body?.toString() || "", "utf8"),
"default": (body?: never) => Buffer.from(JSON.stringify(body) || "", "utf8")
const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer} = {
"application/json": (body) => Buffer.from(JSON.stringify(body) || "", "utf8"),
"text/plain": (body) => Buffer.from(body?.toString() || "", "utf8"),
"default": (body) => Buffer.from(JSON.stringify(body) || "", "utf8")
}

export function httpResponseSerializer(bodySerializerMap?: HttpBodySerializerMap) {
Expand Down
17 changes: 12 additions & 5 deletions packages/rest/src/transform/controller-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getSchemaOrAllOf,
getUnifiedPropertySchemas,
joinSchemas,
moveExamplesToExample,
moveRefsToAllOf,
resolveDiscriminantProperty,
rewriteRefsForOpenApi,
Expand Down Expand Up @@ -224,14 +225,20 @@ export class ControllerMeta {
input: routeInfo.input,
};

OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo));

try {
OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo));
} catch (e) {
if (e instanceof TransformationError) {
throw e;
}
console.error(e);
throw new TransformationError("Could not generate OpenAPI spec for route", modifiedRouteInfo);
}
methods.set(index.method, modifiedRouteInfo);
}

private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document {
const inputSchema = moveRefsToAllOf(route.inputSchema);
const routeIndex = this.getRouteIndex(route);
const dereferencedInputSchema = dereferenceSchema(inputSchema);
const outputSchema = moveRefsToAllOf(route.outputSchema);
const dereferencedOutputSchema = dereferenceSchema(outputSchema);
Expand Down Expand Up @@ -261,8 +268,8 @@ export class ControllerMeta {
},
components: {
schemas: {
...rewriteRefsForOpenApi(inputSchema).definitions,
...rewriteRefsForOpenApi(outputSchema).definitions,
...rewriteRefsForOpenApi(moveExamplesToExample(inputSchema)).definitions,
...rewriteRefsForOpenApi(moveExamplesToExample(outputSchema)).definitions,
},
parameters: {},
},
Expand Down
16 changes: 8 additions & 8 deletions packages/rest/src/transform/error.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import ts from "typescript";
import { RouteIndex } from "./controller-meta";
export class TransformationError extends Error {
constructor(message: string, public readonly routeIndex: RouteIndex, public readonly node?: ts.Node) {
constructor(message: string, routeIndex: RouteIndex, node?: ts.Node) {

Check warning on line 4 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
super(message);
this.message += this.getMessageAddendum();
this.message += this.getMessageAddendum(routeIndex, node);

Check warning on line 6 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

public getMessageAddendum() {
const routeMessage = ` - ${this.routeIndex.method} ${this.routeIndex.path}`;
public getMessageAddendum(routeIndex: RouteIndex, node?: ts.Node) {

Check warning on line 9 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
const routeMessage = ` - ${routeIndex.method} ${routeIndex.path}`;

Check warning on line 10 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
let message = routeMessage;
if (this.node) {
const file: ts.SourceFile = this.node.getSourceFile();
if (node) {
const file: ts.SourceFile = node.getSourceFile();

Check warning on line 13 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const { line, character } = file.getLineAndCharacterOfPosition(
this.node.pos,
node.pos,

Check warning on line 15 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
);
message += `\n${file.fileName}:${line + 1}:${character + 1}`;
}

Check warning on line 18 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 18 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
Expand All @@ -24,6 +24,6 @@ export class StrictTransformationError extends TransformationError {
public readonly warningMessage: string;
constructor(errorMessage: string, warningMessage: string, routeIndex: RouteIndex, node?: ts.Node) {
super(errorMessage, routeIndex, node);
this.warningMessage = warningMessage + this.getMessageAddendum();
this.warningMessage = warningMessage + this.getMessageAddendum(routeIndex, node);

Check warning on line 27 in packages/rest/src/transform/error.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
}
84 changes: 54 additions & 30 deletions packages/rest/src/transform/json-schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function dereferenceSchema(schema: JSONSchema7) {
refParser.schema = clonedSchema;
dereference(refParser, {
dereference: {
circular: true,
circular: "ignore",
onDereference(path: string, value: JSONSchema7) {
(value as { "x-resolved-ref": string })["x-resolved-ref"] = path;
},
Expand Down Expand Up @@ -88,30 +88,37 @@ export function getUnifiedPropertySchemas(
}> = {};
let parentSchemas = 0;

traverse(schema, {
allKeys: false,
cb: {
pre(schema, jsonPtr, rootSchema, parentJsonPtr) {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
if (convertedPath === parentPath && parentJsonPtr != "") {
parentSchemas++;
}
try {
traverse(schema, {
allKeys: false,
cb: {
pre(schema, jsonPtr, rootSchema, parentJsonPtr) {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);
if (convertedPath === parentPath && parentJsonPtr != "") {
parentSchemas++;
}
},
post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);

if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) {
const schemaSet = schemas[keyIndex || ""] || {
schemaSet: [],
required: true,
};
schemaSet.required = !schemaSet.required ? false : parentSchema?.required?.includes(keyIndex) ?? false;
schemaSet.schemaSet.push(schema);
schemas[keyIndex || ""] = schemaSet;
}
},
},
post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => {
const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr);

if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) {
const schemaSet = schemas[keyIndex || ""] || {
schemaSet: [],
required: true,
};
schemaSet.required = !schemaSet.required ? false : parentSchema?.required?.includes(keyIndex) ?? false;
schemaSet.schemaSet.push(schema);
schemas[keyIndex || ""] = schemaSet;
}
},
},
});
});
} catch (e) {
if (e instanceof RangeError) {
throw new Error("Infinite loop detected in json schema");
}
throw e;
}

return Object.fromEntries(
Object.entries(schemas).map(([key, value]) => {
Expand Down Expand Up @@ -165,6 +172,22 @@ export function moveRefsToAllOf(schema: JSONSchema7) {
return clonedSchema;
}

export function moveExamplesToExample(schema: JSONSchema7) {
const clonedSchema = cloneDeep(schema);
traverse(clonedSchema, {
cb: {
pre: (schema) => {
if (schema.example == null && schema.examples != null && schema.examples.length > 0) {
schema.example = schema.examples[0];
delete schema.examples;
}
},
},
});

return clonedSchema;
}

export function rewriteRefsForOpenApi(schema: JSONSchema7) {
const clonedSchema = cloneDeep(schema);
traverse(clonedSchema, {
Expand Down Expand Up @@ -239,18 +262,19 @@ export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: s
const discriminatorValues: string[] = [];

for (const schema of discriminantProperty.schemaSet) {
const resolvedAllOfSchema = getSchemaOrAllOf(schema);
if (
!(schema.type === "string" || schema.type === "number")
|| (schema.const == null && schema.enum == null)
!(resolvedAllOfSchema.type === "string" || resolvedAllOfSchema.type === "number")
|| (resolvedAllOfSchema.const == null && resolvedAllOfSchema.enum == null)
) {
return;
}

if (schema.const != null) {
discriminatorValues.push(schema.const as string);
if (resolvedAllOfSchema.const != null) {
discriminatorValues.push(resolvedAllOfSchema.const as string);
}
if (schema.enum != null) {
discriminatorValues.push(...(schema.enum as string[]));
if (resolvedAllOfSchema.enum != null) {
discriminatorValues.push(...(resolvedAllOfSchema.enum as string[]));
}
}

Expand Down
Loading

0 comments on commit 57c216d

Please sign in to comment.