-1

I'm using Angular 14. I have an abstract model

export class AbstractMyModel {
    constructor(public commonProperty: string) {
    }
}

from which other models will extend. Is there a way to write an interceptor (or other similar function) that will intercept "put" and "post" calls submitted with httpclient and transform the "commonProperty" field of the abstract model? That is, for calls like

return this.httpClient.post<MyResponse>(
  `${url}`,
  child
);

where child is of a type that extends "AbstractMyModel," I would like to apply a function to the child property "commonProperty" such that the equivalent of the below call would be made

return this.httpClient.post<MyResponse>(
  `${url}`,
  {
...child,
    commonProperty: transformFn(child.commonProperty)
  }
);
Dave
  • 15,639
  • 133
  • 442
  • 830

1 Answers1

-1

There are two approaches depending on if you want your interceptor to require "proper" inheritance of the body or if it's enough for the body to have the right shape (see duck typing).

Typescript types don't exist at runtime, so if you want proper inheritance, you'll have to rely on JavaScript's prototype chain. This is handled for you by the extends keyword so long as you use the constructor(s) of the base/derived class(es).

class Base { 
  constructor(commonProperty) {
    this.commonProperty = commonProperty;
  }
}

class Derived extends Base {
  constructor() { super('derived'); }
}

// Using constructors allows for checking 
// inheritance by prototype chain.
const foo = new Derived();
console.log(foo instanceof Base); // true

// Objects that look like derived instances
// are not necessarily derived instances
// (duck typing is not inheritance).
const bar = { commonProperty: 'not derived' };
console.log(bar instanceof Base); // false

If you require proper inheritance, I think you should be able to do this with the vanilla instanceof operator since AbstractMyModel.prototype should show up in the prototype chain for any class Foo extends AbstractMyModel.

Set up an interceptor as usual, check the method and body of the request it receives, then perform whatever logic you need.

import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AbstractMyModel, transformFn } from '../models/abstract-my.model';

const supportedMethods = ['PUT', 'POST'];

export class MyModelInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    // Short circuit if the request doesn't match
    // our criteria.
    const methodUnsupported = !supportedMethods.some(m => request.method === m);
    const bodyTypeUnsupported = !(request.body instanceof AbstractMyModel);
    
    if (methodUnsupported || bodyTypeUnsupported) {
      return next.handle(request);
    }

    // Otherwise perform whatever logic we need.
    const commonProperty = transformFn(request.body.commonProperty);
    const newBody = { ...request.body, commonProperty };
    const newRequest = request.clone({ body: newBody });

    return next.handle(newRequest);
  }
}

If the shape of the body is sufficient (you don't need instance methods provided by AbstractMyModel), you can check for the existence of the property you care about.

Again, there are two ways: Object.hasOwnProperty() or the in operator and the difference is discussed in this question: "if (key in object) or if(object.hasOwnProperty(key)". In this case, you want 'commonProperty' in request.body since the object doesn't need to have its own copy of commonProperty - an inherited version is sufficient.

import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AbstractMyModel, transformFn } from '../models/abstract-my.model';

const supportedMethods = ['PUT', 'POST'];

export class MyModelInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    // Short circuit if the request doesn't match
    // our criteria.
    const methodUnsupported = !supportedMethods.some(m => request.method === m);
    const bodyTypeUnsupported = !('commonProperty' in request.body);
    
    if (methodUnsupported || bodyTypeUnsupported) {
      return next.handle(request);
    }

    // Otherwise perform whatever logic we need.
    const commonProperty = transformFn(request.body.commonProperty);
    const newBody = { ...request.body, commonProperty };
    const newRequest = request.clone({ body: newBody });

    return next.handle(newRequest);
  }
}
D M
  • 5,769
  • 4
  • 12
  • 27
  • hmmm, the "instanceof" isn't working despite the fact I can see my request.body has all the attributes of this object. – Dave Mar 27 '23 at 14:41
  • Are you creating the initial body using a constructor for a class that extends your abstract model? `instanceof` relies on the native JS prototype chain - typing the object is not enough to make an instance. (That is, `foo = new MyDerivedModel()` works, while `foo: MyDerivedModel = {}` does not). – D M Mar 27 '23 at 14:51
  • The object being submitted with the POST request is coming from a reactive form -- "const myObj = this.form.value" where "form" is an instance of "FormGroup" and all the fields in the form are fields from the object I want to intercept. – Dave Mar 27 '23 at 15:00