Skip to content

Commit

Permalink
updated estdlib and removed requirement for @ReturnType
Browse files Browse the repository at this point in the history
deserializes now automatically depending on real return data (including arrays and primitives),
optionally taking into account the @ReturnType() for custom class instances.
fixed handling observable error handling
  • Loading branch information
marcj committed Mar 15, 2019
1 parent 5dea040 commit 44c8bca
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 109 deletions.
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"rxjs": "6.3.3"
},
"dependencies": {
"@marcj/estdlib": "^0.1.6-",
"@marcj/estdlib": "^0.1.7",
"dot-prop": "^4.2.0",
"ws": "^6.1.2"
},
Expand Down
39 changes: 25 additions & 14 deletions packages/client/src/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ServerMessageResult,
StreamBehaviorSubject
} from "@marcj/glut-core";
import {applyDefaults, eachKey} from "@marcj/estdlib";
import {applyDefaults, eachKey, isArray} from "@marcj/estdlib";
import {EntityState} from "./entity-state";
import {tearDown} from "@marcj/estdlib-rxjs";

Expand Down Expand Up @@ -251,6 +251,25 @@ export class SocketClient {
args: args
});

function deserializeResult(next: any): any {
if (types.returnType.type === 'Entity') {
const classType = RegisteredEntities[types.returnType.entityName!];
if (!classType) {
reject(new Error(`Entity ${types.returnType.entityName} now known on client side.`));
subject.close();
return;
}

if (isArray(next)) {
return next.map(v => plainToClass(classType, v));
} else {
return plainToClass(classType, next);
}
}

return next;
}

subject.subscribe((reply) => {
if (reply.type === 'type') {
if (reply.returnType === 'subject') {
Expand Down Expand Up @@ -336,29 +355,19 @@ export class SocketClient {
}

if (reply.type === 'next/json') {
if (reply.entityName && RegisteredEntities[reply.entityName]) {
reply.next = plainToClass(RegisteredEntities[reply.entityName], reply.next);
}
resolve(reply.next);
resolve(deserializeResult(reply.next));
}

if (reply.type === 'next/observable') {
if (reply.entityName && RegisteredEntities[reply.entityName]) {
reply.next = plainToClass(RegisteredEntities[reply.entityName], reply.next);
}

if (subscribers[reply.subscribeId]) {
subscribers[reply.subscribeId].next(reply.next);
subscribers[reply.subscribeId].next(deserializeResult(reply.next));
}
}

if (reply.type === 'next/subject') {
if (reply.entityName && RegisteredEntities[reply.entityName]) {
reply.next = plainToClass(RegisteredEntities[reply.entityName], reply.next);
}

if (streamBehaviorSubject) {
streamBehaviorSubject.next(reply.next);
streamBehaviorSubject.next(deserializeResult(reply.next));
}
}

Expand Down Expand Up @@ -395,6 +404,7 @@ export class SocketClient {
subscribers[reply.subscribeId].error(reply.error);
}

delete subscribers[reply.subscribeId];
subject.close();
}

Expand All @@ -403,6 +413,7 @@ export class SocketClient {
subscribers[reply.subscribeId].complete();
}

delete subscribers[reply.subscribeId];
subject.close();
}
});
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"rxjs": "6.3.3"
},
"dependencies": {
"@marcj/estdlib": "^0.1.6",
"@marcj/estdlib-rxjs": "^0.1.6"
"@marcj/estdlib": "^0.1.7",
"@marcj/estdlib-rxjs": "^0.1.7"
},
"devDependencies": {
"@marcj/marshal": "^0.5.1",
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,19 @@ export interface ServerMessageNextJson {
type: 'next/json';
id: number;
next: any;
entityName?: string;
}

export interface ServerMessageNextObservable {
type: 'next/observable';
id: number;
next: any;
subscribeId: number;
entityName?: string;
}

export interface ServerMessageNextSubject {
type: 'next/subject';
id: number;
next: any;
entityName?: string;
}

export interface ServerMessageNextCollection {
Expand Down
4 changes: 2 additions & 2 deletions packages/integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"tsc-watch": "echo nothing"
},
"dependencies": {
"@marcj/estdlib": "^0.1.6",
"@marcj/estdlib-rxjs": "^0.1.6",
"@marcj/estdlib": "^0.1.7",
"@marcj/estdlib-rxjs": "^0.1.7",
"@marcj/glut-client": "^0.0.4",
"@marcj/glut-core": "^0.0.4",
"@marcj/glut-server": "^0.0.4",
Expand Down
50 changes: 45 additions & 5 deletions packages/integration/tests/controller-basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ test('test basic serialisation: primitives', async () => {
@Controller('test')
class TestController {
@Action()
@ReturnType(String)
names(last: string): string[] {
return ['a', 'b', 'c', 15 as any as string, last];
}
Expand All @@ -89,8 +90,7 @@ test('test basic serialisation: primitives', async () => {
const test = client.controller<TestController>('test');
const names = await test.names(16 as any as string);

//we do not convert primitives as typescript checks that already at build time.
expect(names).toEqual(['a', 'b', 'c', 15, 16]);
expect(names).toEqual(['a', 'b', 'c', "15", "16"]);

await close();
});
Expand All @@ -99,17 +99,55 @@ test('test basic serialisation return: entity', async () => {
@Controller('test')
class TestController {
@Action()
@ReturnType(User)
async user(name: string): Promise<User> {
return new User(name);
}

@Action()
@ReturnType(User)
async users(name: string): Promise<User[]> {
return [new User(name)];
}

@Action()
async failUser(name: string): Promise<User> {
return new User(name);
}

@Action()
async failObservable(name: string): Promise<Observable<User>> {
return new Observable((observer) => {
observer.next(new User(name));
});
}
}

const {client, close} = await createServerClientPair('test basic setup', [TestController]);
const {client, close} = await createServerClientPair('test basic serialisation return: entity', [TestController]);

const test = client.controller<TestController>('test');
const user = await test.user('peter');
expect(user).toBeInstanceOf(User);
console.log('user', user);

const users = await test.users('peter');
expect(users.length).toBe(1);
expect(users[0]).toBeInstanceOf(User);

try {
await test.failUser('peter');
fail('Should fail');
} catch (e) {
expect(e).toMatch('Action test::failUser failed: Error: Result returns an not annotated object (User) that can not be serialized. ' +
'Use e.g. @ReturnType(MyClass) at your action');
}

try {
await (await test.failObservable('peter')).toPromise();
fail('Should fail');
} catch (e) {
expect(e).toMatch('Action test::failObservable failed: Observable returns an not annotated object (User) that can not be serialized. ' +
'Use e.g. @ReturnType(MyClass) at your action.');
}

await close();
});
Expand All @@ -123,7 +161,7 @@ test('test basic serialisation param: entity', async () => {
}
}

const {client, close} = await createServerClientPair('test basic setup', [TestController]);
const {client, close} = await createServerClientPair('test basic serialisation param: entity', [TestController]);

const test = client.controller<TestController>('test');
const userValid = await test.user(new User('peter2'));
Expand All @@ -141,6 +179,7 @@ test('test basic promise', async () => {
}

@Action()
@ReturnType(User)
async user(name: string): Promise<User> {
return new User(name);
}
Expand Down Expand Up @@ -184,6 +223,7 @@ test('test observable', async () => {
}

@Action()
@ReturnType(User)
user(name: string): Observable<User> {
return new Observable((observer) => {
observer.next(new User('first'));
Expand Down
4 changes: 2 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"rxjs": "6.3.3"
},
"dependencies": {
"@marcj/estdlib": "^0.1.6",
"@marcj/estdlib-rxjs": "^0.1.6",
"@marcj/estdlib": "^0.1.7",
"@marcj/estdlib-rxjs": "^0.1.7",
"@marcj/marshal-mongo": "^0.5.3",
"clone": "^2.1.1",
"fs-extra": "^6.0.1",
Expand Down
92 changes: 76 additions & 16 deletions packages/server/src/client-connection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Injectable, Injector} from "injection-js";
import {Observable} from "rxjs";
import {Application, SessionStack} from "./application";
import {ClientMessageAll, ServerMessageActionType} from "@marcj/glut-core";
import {ClientMessageAll, Collection, EntitySubject, ServerMessageActionType} from "@marcj/glut-core";
import {ConnectionMiddleware} from "./connection-middleware";
import {ConnectionWriter} from "./connection-writer";
import {arrayRemoveItem, each, eachKey} from "@marcj/estdlib";
import {arrayRemoveItem, each, eachKey, isArray, isObject, isPlainObject, getClassName} from "@marcj/estdlib";
import {getActionParameters, getActionReturnType, getActions} from "./decorators";
import {plainToClass, RegisteredEntities, validate} from "@marcj/marshal";
import {plainToClass, RegisteredEntities, validate, classToPlain} from "@marcj/marshal";
import {map} from "rxjs/operators";

type ActionTypes = { parameters: ServerMessageActionType[], returnType: ServerMessageActionType };

Expand Down Expand Up @@ -176,38 +177,97 @@ export class ClientConnection {

const errors = await validate(RegisteredEntities[type.entityName], args[i]);
if (errors.length) {
//todo, wrapp in own ValidationError so we can serialise it better when send to the client
//todo, wrap in own ValidationError so we can serialise it better when send to the client
throw new Error(`${fullName} validation failed: ` + JSON.stringify(errors));
}
args[i] = plainToClass(RegisteredEntities[type.entityName], args[i]);
}
}

try {
return (controllerInstance as any)[methodName](...args);
let result = (controllerInstance as any)[methodName](...args);

if (typeof (result as any)['then'] === 'function') {
// console.log('its an Promise');
result = await result;
}

if (result instanceof EntitySubject) {
return result;
}

if (result instanceof Collection) {
return result;
}

const converter = {
'Entity': (v) => {
return classToPlain(RegisteredEntities[types.returnType.entityName!], v);
},
'Boolean': (v) => {
return Boolean(v);
},
'Number': (v) => {
return Number(v);
},
'String': (v) => {
return String(v);
},
'Object': (v) => {
return v;
}
};

function checkForNonObjects(v: any, prefix: string = 'Result') {
if (isArray(v) && v[0]) {
v = v[0];
}

if (isObject(v) && !isPlainObject(v)) {
throw new Error(`${prefix} returns an not annotated object (${getClassName(v)}) that can not be serialized. Use e.g. @ReturnType(MyClass) at your action.`);
}
}

if (result instanceof Observable) {
return result.pipe(map((v) => {
if (types.returnType.type === 'undefined') {
checkForNonObjects(v, `Action ${fullName} failed: Observable`);

return v;
}

if (isArray(v)) {
return v.map(j => converter[types.returnType.type](j));
}

return converter[types.returnType.type](v);
}));
}

if (types.returnType.type === 'undefined') {
checkForNonObjects(result);

return result;
}

if (isArray(result)) {
return result.map(v => converter[types.returnType.type](v));
}

return converter[types.returnType.type](result);
} catch (error) {
// possible security whole, when we send all errors.
console.error(error);
throw new Error(`Action ${fullName} failed: ${error}`);
}
}

console.error('Action unknown', fullName);
throw new Error(`Action unknown ${fullName}`);
}

public async actionSend(message: ClientMessageAll, exec: (() => Promise<any> | Observable<any>)) {
try {
let result = exec();

if (typeof (result as any)['then'] === 'function') {
// console.log('its an Promise');
result = await result;
}

await this.connectionMiddleware.actionMessageOut(message, result);
await this.connectionMiddleware.actionMessageOut(message, await exec());
} catch (error) {
console.log('Worker execution error', message, error);
await this.writer.sendError(message.id, error);
}
}
Expand Down

0 comments on commit 44c8bca

Please sign in to comment.