From 9ac824d3bb966c4320474b308d5ea2db1cfa7c39 Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 17 Feb 2023 18:10:00 +0100 Subject: [PATCH] feature(http): silently serialize unpopulated references Try to detect class type arrays if type was not annotated explicitly. --- packages/http/src/http.ts | 69 ++++++++++++++++++++++++------ packages/http/tests/router.spec.ts | 62 ++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/packages/http/src/http.ts b/packages/http/src/http.ts index 11c3aac63..29bbd3da8 100644 --- a/packages/http/src/http.ts +++ b/packages/http/src/http.ts @@ -8,7 +8,7 @@ * You should have received a copy of the MIT License along with this program. */ -import { asyncOperation, ClassType, CustomError, getClassName, getClassTypeFromInstance, isClassInstance } from '@deepkit/core'; +import { asyncOperation, ClassType, CustomError, getClassName, getClassTypeFromInstance, isArray, isClassInstance } from '@deepkit/core'; import { OutgoingHttpHeaders, ServerResponse } from 'http'; import { eventDispatcher } from '@deepkit/event'; import { HttpRequest, HttpResponse } from './model'; @@ -18,7 +18,19 @@ import { HttpRouter, RouteConfig, RouteParameterResolverForInjector } from './ro import { createWorkflow, WorkflowEvent } from '@deepkit/workflow'; import type { ElementStruct, render } from '@deepkit/template'; import { FrameCategory, Stopwatch } from '@deepkit/stopwatch'; -import { getSerializeFunction, hasTypeInformation, ReflectionKind, resolveReceiveType, SerializationError, serialize, serializer, ValidationError } from '@deepkit/type'; +import { + getSerializeFunction, + hasTypeInformation, + ReflectionKind, + resolveReceiveType, + SerializationError, + serialize, + serializer, + Type, + typeSettings, + UnpopulatedCheck, + ValidationError +} from '@deepkit/type'; import stream from 'stream'; export function isElementStruct(v: any): v is ElementStruct { @@ -381,7 +393,20 @@ export class JSONResponse extends BaseResponse { } } -export type SupportedHttpResult = undefined | null | number | string | Response | JSONResponse | HtmlResponse | HttpResponse | ServerResponse | stream.Readable | Redirect | Uint8Array | Error; +export type SupportedHttpResult = + undefined + | null + | number + | string + | Response + | JSONResponse + | HtmlResponse + | HttpResponse + | ServerResponse + | stream.Readable + | Redirect + | Uint8Array + | Error; export interface HttpResultFormatterContext { request: HttpRequest; @@ -424,9 +449,14 @@ export class HttpResultFormatter { } handleUnknown(result: any, context: HttpResultFormatterContext): void { - this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType); - - context.response.end(JSON.stringify(result)); + const oldCheck = typeSettings.unpopulatedCheck; + try { + typeSettings.unpopulatedCheck = UnpopulatedCheck.None; + this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType); + context.response.end(JSON.stringify(result)); + } finally { + typeSettings.unpopulatedCheck = oldCheck; + } } handleHtmlResponse(result: HtmlResponse, context: HttpResultFormatterContext): void { @@ -447,11 +477,13 @@ export class HttpResultFormatter { } handleTypeEntity(classType: ClassType, instance: T, context: HttpResultFormatterContext, route?: RouteConfig): void { - this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType); + this.handleType(resolveReceiveType(classType), instance, context, route); + } + handleType(type: Type, instance: T, context: HttpResultFormatterContext, route?: RouteConfig): void { + this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType); const serializerToUse = route && route?.serializer ? route.serializer : serializer; - - context.response.end(JSON.stringify(serialize(instance, route ? route.serializationOptions : undefined, serializerToUse, undefined, resolveReceiveType(classType)))); + context.response.end(JSON.stringify(serialize(instance, route ? route.serializationOptions : undefined, serializerToUse, undefined, type))); } handleStream(stream: stream.Readable, context: HttpResultFormatterContext): void { @@ -491,6 +523,19 @@ export class HttpResultFormatter { this.handleTypeEntity(classType, result, context); return; } + } else if (isArray(result) && result.length > 0 && isClassInstance(result[0])) { + const firstClassType = getClassTypeFromInstance(result[0]); + let allSameType = true; + for (const item of result) { + if (!isClassInstance(item) || getClassTypeFromInstance(item) !== firstClassType) { + allSameType = false; + break; + } + } + if (allSameType && hasTypeInformation(firstClassType)) { + this.handleType({ kind: ReflectionKind.array, type: resolveReceiveType(firstClassType) }, result, context); + return; + } } this.handleUnknown(result, context); @@ -660,9 +705,9 @@ export class HttpListener { if (result instanceof stream.Readable) { const stream = result as stream.Readable; await new Promise((resolve, reject) => { - stream.once('readable', resolve) - stream.once('error', reject) - }) + stream.once('readable', resolve); + stream.once('error', reject); + }); } const responseEvent = new HttpResponseEvent(event.injectorContext, event.request, event.response, result, event.route); responseEvent.controllerActionTime = Date.now() - start; diff --git a/packages/http/tests/router.spec.ts b/packages/http/tests/router.spec.ts index a0a6355bf..f5b1f0d28 100644 --- a/packages/http/tests/router.spec.ts +++ b/packages/http/tests/router.spec.ts @@ -6,7 +6,7 @@ import { eventDispatcher } from '@deepkit/event'; import { HttpBody, HttpBodyValidation, HttpQueries, HttpQuery, HttpRegExp, HttpRequest } from '../src/model'; import { getClassName, isObject, sleep } from '@deepkit/core'; import { createHttpKernel } from './utils'; -import { Group, MinLength, PrimaryKey, Reference } from '@deepkit/type'; +import { Excluded, Group, MinLength, PrimaryKey, Reference, typeSettings, UnpopulatedCheck } from '@deepkit/type'; test('router', async () => { class Controller { @@ -927,6 +927,66 @@ test('BodyValidation', async () => { expect((await httpKernel.request(HttpRequest.POST('/action3').json({ username: 'Pe' }))).bodyString).toEqual(`{"message":"Invalid: Min length is 3"}`); }); +test('unpopulated entity without type information', async () => { + interface Group { + id: number & PrimaryKey; + } + + class User { + invisible: boolean & Excluded = false; + constructor(public id: number, public group: Group & Reference) { + } + } + + + function disableReference(o: User) { + //we simulate an unpopulated reference + Object.defineProperty(o, 'group', { + get() { + if (typeSettings.unpopulatedCheck === UnpopulatedCheck.Throw) { + throw new Error(`Reference group was not populated. Use joinWith(), useJoinWith(), etc to populate the reference.`); + } + } + }); + } + + class Controller { + @http.GET('/1') + action1() { + const o = new User(2, undefined as any); + disableReference(o); + return [o]; + } + + @http.GET('/2') + async action2(): Promise { + const o = new User(2, undefined as any); + disableReference(o); + return [o]; + } + + @http.GET('/3').response(200) + async action3() { + const o = new User(2, undefined as any); + disableReference(o); + return [o]; + } + + @http.GET('/4') + async action4() { + const o = new User(2, undefined as any); + disableReference(o); + return [o, {another: 3}]; + } + } + + const httpKernel = createHttpKernel([Controller]); + expect((await httpKernel.request(HttpRequest.GET('/1'))).json).toEqual([{ id: 2 }]); + expect((await httpKernel.request(HttpRequest.GET('/2'))).json).toEqual([{ id: 2 }]); + expect((await httpKernel.request(HttpRequest.GET('/3'))).json).toEqual([{ id: 2 }]); + expect((await httpKernel.request(HttpRequest.GET('/4'))).json).toEqual([{ id: 2, invisible: false }, {another: 3}]); +}); + //disabled for the moment since critical functionality has been removed // test('stream', async () => { // class Controller {