Skip to content

Commit

Permalink
feature(http): silently serialize unpopulated references
Browse files Browse the repository at this point in the history
Try to detect class type arrays if type was not annotated explicitly.
  • Loading branch information
marcj committed Feb 17, 2023
1 parent 10868e4 commit 9ac824d
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 13 deletions.
69 changes: 57 additions & 12 deletions packages/http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -447,11 +477,13 @@ export class HttpResultFormatter {
}

handleTypeEntity<T>(classType: ClassType<T>, instance: T, context: HttpResultFormatterContext, route?: RouteConfig): void {
this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType);
this.handleType(resolveReceiveType(classType), instance, context, route);
}

handleType<T>(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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
62 changes: 61 additions & 1 deletion packages/http/tests/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<User[]> {
const o = new User(2, undefined as any);
disableReference(o);
return [o];
}

@http.GET('/3').response<User[]>(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 {
Expand Down

0 comments on commit 9ac824d

Please sign in to comment.