troyanskiy / ngx-resource

Resource (REST) Client for Angular 2
http://troyanskiy.github.io/ngx-resource/
200 stars 46 forks source link

[QUESTION/ISSUE] Converting circular structure to json #126

Closed Melzar closed 7 years ago

Melzar commented 7 years ago

Hello, I'm not sure if I found an issue or is it by design.

I'm receiving following error

ERROR Error: Uncaught (in promise): TypeError: Converting circular structure to JSON
TypeError: Converting circular structure to JSON
TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at AssetService.webpackJsonp.../../../../ngx-resource/src/Resource.js.Resource.$_prepareBody (Resource.js:351)
    at Resource.js:219
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
    at Object.onInvoke (core.es5.js:3933)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
    at zone.js:818
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3924)
    at JSON.stringify (<anonymous>)
    at AssetService.webpackJsonp.../../../../ngx-resource/src/Resource.js.Resource.$_prepareBody (Resource.js:351)
    at Resource.js:219
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
    at Object.onInvoke (core.es5.js:3933)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
    at zone.js:818
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3924)
    at resolvePromise (zone.js:770)
    at zone.js:821
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
    at Object.onInvokeTask (core.es5.js:3924)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:423)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask (zone.js:191)
    at drainMicroTaskQueue (zone.js:584)
    at HTMLFormElement.ZoneTask.invoke (zone.js:490)

Steps to reproduce it is pretty easy.

First you have to fetch data ( for example single object by id ) using ResourceMethod

 get: ResourceMethod<{
        id: any;
    }, TFull>;

and then received object by for example


.$observable.subscribe(
      (item: Item) => {

post again like

myservice.update(myItem).$observable.subscribe....

From my research I've noticed that when we are fetching data with use of ResourceMethod then received object has additional property $resource and then again if we post the same object then this property cause circular error. If we remove manually $resource property like

delete item['$resource'] then everything is fine.

So my question here is if this behaviour is by design or is it a bug? Is there any chance to avoid this error by using ResourceMethod response?

My Project dependencies

node 7.10.0

"dependencies": {
    "@angular/animations": "^4.2.5",
    "@angular/common": "^4.2.5",
    "@angular/compiler": "^4.2.5",
    "@angular/core": "^4.2.5",
    "@angular/forms": "^4.2.5",
    "@angular/http": "^4.2.5",
    "@angular/platform-browser": "^4.2.5",
    "@angular/platform-browser-dynamic": "^4.2.5",
    "@angular/router": "^4.2.5",
    "angular2-jwt": "^0.2.3",
    "angular2-materialize": "^15.0.4",
    "core-js": "^2.4.1",
    "hammerjs": "^2.0.8",
    "jquery": "^3.1.1",
    "materialize-css": "^0.98.2",
    "moment": "^2.17.1",
    "ng2-select-compat": "^1.3.1",
    "ngx-pagination": "^3.0.1",
    "ngx-resource": "^3.2.1",
    "rxjs": "^5.4.1",
    "zone.js": "^0.8.12"
  },
  "devDependencies": {
    "@angular/cli": "^1.2.0",
    "@angular/compiler-cli": "^4.2.5",
    "@angular/language-service": "^4.2.5",
    "@types/hammerjs": "^2.0.34",
    "@types/jasmine": "^2.5.53",
    "@types/node": "^6.0.79",
    "codelyzer": "^3.1.1",
    "jasmine-core": "^2.6.4",
    "jasmine-spec-reporter": "^4.1.1",
    "karma": "^1.7.0",
    "karma-chrome-launcher": "^2.2.0",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.3.0",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "^5.1.2",
    "ts-node": "^3.1.0",
    "tslint": "^5.4.3",
    "typescript": "2.3.4"
  }

This behaviour has been introduced with following commit

https://github.com/troyanskiy/ngx-resource/commit/4f2319a2e5443722eeffa7f1f0901916287ee197

I would be grateful for any help provided on that matter :)

troyanskiy commented 7 years ago

Hello.

I think it's related to the JSON.stringify. The provided json can be stringify becouse it has circular attributes on itself. Something like that

let d = {
  data: 'Hello'
};
d.me = d;

JSON.stringify(d); // that will throw and error Converting circular structure to JSON.

So please check your json object.

Melzar commented 7 years ago

Thank you for your quick response.

I've performed deeper 'inception' into source code and the issue was related to partially to new $resource property but the main reason was related to dependency injection and injecting router which caused issue which I've described.

Thanks again fro your help.

troyanskiy commented 7 years ago

Sorry... I have read the issue quickly, I'm on AngularCamp now. I will check deeper the problem a bit latter. Could you please your resource class here or make some plunker. Thank you!

Melzar commented 7 years ago

Not a problem. I've solve this issue but as a hint - If you define a constructor with dependency injection in your service and provided dependency will be router or http service then you will face issue which I've described.

I've solved this issue just by removing constructor ( actually I could do this in my case but for other cases it might not work as expected ).

Making things short :

Inject in constructor for example http: Http. Then it will be available in your $resource property. Then just follow my post above to reproduce this issue.

Example service

@Injectable()
@ResourceParams({
  url: 'my url'
})
export class MyService extends ResourceCRUD<any, any, any> {

  @ResourceAction({
    path: '/item/{!id}',
  })
  getItem: ResourceMethod<{
    id: any;
  }, Item[]>;

 constructor(private router: Router, private http: Http ){ 
   super(http);
 }
}
ghost commented 7 years ago

I've got the same problem.

I use ngx-resource like this:

@Injectable()
@ResourceParams({
  withCredentials: true,
  url: BASE_URL + '/v2',
})
export class RestClient extends Resource {
  constructor(http: Http,
              private toasterService: ToasterService,
              private slimLoadingBarService: SlimLoadingBarService) {
    super(http);
  }

  // tslint:disable-next-line function-name
  $requestInterceptor(request: Request, methodOptions?: ResourceActionBase): Request {
    this.slimLoadingBarService.start();
    return request;
  }
....

then I use this service like this:

@Injectable()
export class AppearanceApiService extends RestClient {
  @ResourceAction({ path: '/main/appearance' })
  get: ResourceMethod<{ type: string }, AppearanceResponse>;

  @ResourceAction({ path: '/main/appearance', method: RequestMethod.Put })
  update: ResourceMethod<types.Appearance, BaseResponse>;
}
appearanceApiService.get(
  { type: $transition$.params().type },
  (response: AppearanceResponse) => {
    !!('$resource' in response.$resource); // true
    !!('$resolved' in response.$resolved); // true
    !!('$observable' in response.$observable); // true
    !!('$abortRequest' in response.$abortRequest); // true
    appearanceDataService.appearance = response;
  },
);

and then I send the response back using appearanceApiService.update

  1. Is it correct that I have the $ properties in the response?
  2. I think something's wrong with $cleanData function when custom constructor is being used.
troyanskiy commented 7 years ago

@Melzar @maxkorz Please try updated version. It should be fine now. @maxkorz Answers:

  1. Yes. If you don't want to have $ please check lean param in the readme
  2. I have update $cleanData method
ghost commented 7 years ago

@troyanskiy thanks, it works.

  1. lean param removes $ from everywhere. But is it possible to remove $ from response but keep it in ResourceResult<O>? So I could do something like that:

    public ngOnDestroy(): void {
    if (this.apiRequest && !this.apiRequest.$resolved) {
      this.apiRequest.$abortRequest();
    }
    }
    
    public getMyStuff(): void {
    this.apiRequest = this.myApiService.query(
      (response) => {
        console.log(Object.keys(response)); // I don't want to have $ variables here
      },
    ); // but I want $ variables in ResourceResult
    }
troyanskiy commented 7 years ago

@maxkorz personally I for one of the projects I'm using it like that But in that case you don't have $ variables at all.

@ResourceParams({
  toPromise: true, // all methods will return promise instead of ResourceResult. ResourceResult will be return by promise
  lean: true // removes from resource result $
})
export class MyRes extends Resource {
  @ResourceAction({
    path: '/item',
    toPromise: true, // or you can add that per method
    lean: true // that also can be added per method
  })
  getItem: ResourceMethodPromise<void, any>;
}

public async getMyStuff(): void {
  const data = await this.myRes.getItem();
}
troyanskiy commented 7 years ago

Or simply clean response using Resource.$cleanData static method;

public ngOnDestroy(): void {
    if (this.apiRequest && !this.apiRequest.$resolved) {
      this.apiRequest.$abortRequest();
    }
  }

  public getMyStuff(): void {
    this.apiRequest = this.myApiService.query(
      (response) => {
        response = Resource.$cleanData(response); // it will create new object without $
      },
    ); // but I want $ variables in ResourceResult
  }