#Help in resolving Generic providers.

13 messages · Page 1 of 1 (latest)

valid panther
#

I am currently building an API that requires me to follows JSONAPI schema.

To achieve the correct serialized response, I make use of the ts-japi package. This package allows me to configure serializers for each specific resource/entity.

I make use of this in conjunction with nest interceptors so that my entities can simply be returned from controllers, and the interceptor can serialize the response in JSONAPI format.

My jsonapi interceptor depends on serializers I've mentioned before. The jsonapi serializers dependencies that I provide are generic types.

My approach was working fine when I was registering only 1 serializer per module.

The problem is, when I now register serializers for two different entities, the resolved serializer provided to the jsonapi interceptor is always the first one in the providers array even if I have specified the correct interceptor in the controller.

Code snippet to follow.

#

jsonapi-response.interceptor.ts

@Injectable()
export class JsonApiResponseInterceptor<T extends Dictionary<any>>
  implements NestInterceptor<T, any>
{
  constructor(private readonly serializer: Serializer<T>) { }
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<any> | Promise<Observable<any>> {
    const { include } = context.getArgByIndex(0).query;

    return next.handle().pipe(
      map(async (data: any) => {
        let pagingMeta = undefined;
        let entityData = data;
        if (
          data instanceof Array &&
          data.length === 2 &&
          data[0] instanceof Array
        ) {
          const [entities, { number, size, total }] = data;
          pagingMeta = new Metaizer(() => ({
            pagination: { number, size, total },
          }));
          entityData = entities;
        }

        return await this.serializer.serialize(entityData, {
          include: include?.split(','),
          metaizers: { document: pagingMeta },
        });
      }),
    );
  }
}```

**foo.serializer.ts**

export const FooSerializer = new Serializer<Foo>('foo', {
projection: {
created: 1,
updated: 1,
title: 1,
description: 1,
},
});

export const createFooSerializer = () => FooSerializer;


**bar.serializer.ts**

export const BarSerializer = new Serializer<Bar>('bar', {
projection: {
created: 1,
updated: 1,
title: 1,
description: 1,
},
});

export const createBarSerializer = () => BarSerializer;


**foobar.module.ts**

@Module({
controllers: [FooBarController],
providers: [
{
provide: Serializer<Foo>,
useFactory: createFooSerializer ,
},
{
provide: Serializer<Bar>,
useFactory: createBarSerializer ,
},
],
})
export class FooBarModule {}

#

foobar.controller.ts

@Controller('foobar')
export class FooBarController {
  @UseInterceptors(JsonApiResponseInterceptor<Foo>)
  async GetFoo(): Promise<Foo> {
    return new Foo();
  }


  @UseInterceptors(JsonApiResponseInterceptor<Bar>)
  async GetBar(): Promise<Bar> {
    return new Bar();
  }
}
serene vine
#

Generics don't work for injection tokens. They end up being Serializer and not SerializerFoo. What you might want to do is create a provider that is an array of serializers and then filter to whichever is the correct serializer inside of the interceptor

valid panther
#

Is there a way for me to specify the "filter" when decorating my controller with the interceptor?
For example

my.module.ts

...
providers: [
  {
    provide: 'JSONAPI_INTERCEPTOR',
    useFactory: buildDictionaryOfSerializers
  }
]
...

jsonapi.interceptor.ts

...
intercept(...) {
  ...
  return this.serializer[entityType].serialize(entity);
  ...
}
...

my.controller.ts

export class MyController{
  @UseInterceptor(JsonApiResponseInterceptor('foo'))
  async GetFoo(): Promise<Foo> {
    return new Foo();
  }
}

?

serene vine
#

You can use reflection to get the entityType. It'll mean having the data there twice (once for Typescript's return type once for the runtime metadata, but it's better than nothing

#

Or, if you return entity instances you can get the entity instance type from the entity itself by its prototype (IIRC)

valid panther
serene vine
#

Oh, IIRC is "if I recall correctly"

valid panther
#

Aa. Alright.

I'll try it out.

Thanks for the quick reply! 😃 👍

valid panther
# serene vine Oh, IIRC is "if I recall correctly"

I ended up following the build-in ClassSerializerInterceptor.

jsonapi-response.interceptor.ts

export const JSONAPI_SERIALIZERS = 'JSONAPI_SERIALIZERS';

export const JSONAPI_SERIALIZER_OPTIONS = 'jsonapi_interceptor:options';

export interface IJsonApiSerializerOptions {
  entityType: InjectionToken;
}

const REFLECTOR = 'Reflector';

@Injectable()
export class JsonApiResponse2Interceptor implements NestInterceptor {
  constructor(
    @Inject(REFLECTOR) protected readonly reflector: any,

    @Inject(JSONAPI_SERIALIZERS)
    private readonly serializers: Map<InjectionToken, () => Serializer>,
  ) {}

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<any> | Promise<Observable<any>> {
    const contextOptions = this.getContextOptions(context);
    const { include } = context.getArgByIndex(0).query;

    return next.handle().pipe(
      map(async (data: any) => {
        let pagingMeta = undefined;
        let entityData = data;
        
        ...otherstuff

        const serializer = this.serializers.get(
          contextOptions?.entityType as InjectionToken,
        );

        if (serializer) {
          return await serializer().serialize(entityData, {
            include: include?.split(','),
            metaizers: { document: pagingMeta },
          });
        }

        return data;
      }),
    );
  }

  protected getContextOptions(
    context: ExecutionContext,
  ): IJsonApiSerializerOptions | undefined {
    return this.reflector.getAllAndOverride(JSONAPI_SERIALIZER_OPTIONS, [
      context.getHandler(),
      context.getClass(),
    ]);
  }
}

jsonapi-serializer-options.decorator.ts

export const JsonApiSerializeOptions = (options: IJsonApiSerializerOptions) =>
  SetMetadata(JSONAPI_SERIALIZER_OPTIONS, options);
#

mymodule.provider.ts

...
  {
    provide: JSONAPI_SERIALIZERS,
    useFactory: () => {
      const serializers = new Map<InjectionToken, () => Serializer>();
      serializers.set(Entity1, createEntity1Serializer);
      serializers.set(Entity2, createEntity2Serializer);
      return serializers;
    },
  },
...

mymodule.controller.ts

  @UseInterceptors(JsonApiResponse2Interceptor)
  @JsonApiSerializeOptions({ entityType: Entity1})
  @Get('entity1s')
  async GetStuff(): Promise<Entity1[] | [Entity1[], IPaging]> {
    this.service.getstuff();
  }

  @UseInterceptors(JsonApiResponse2Interceptor)
  @JsonApiSerializeOptions({ entityType: Entity2})
  @Get('entity2s')
  async GetStuff2(): Promise<Entity1[] | [Entity1[], IPaging]> {
    this.service.getstuff();
  }
#

Thanks @serene vine.