#Documentation of a dynamic 'filter' query parameter, possibly with the help of class-validator?

1 messages · Page 1 of 1 (latest)

grave wharf
#

Hello there! I'm having a situation where I'm exposing a filter query parameter according to the JSON:API specification which allows consumers to filter any of the resource's properties. So far so good! Now I'd like to document that query parameter in Swagger.

For both the validation via class-validator and the actual filtering I'm using a class that looks like so:

export class SomeFilterQuery {
  IsNumber()
  someNumber?: number;
}

Now I thought of doing something like this in the controller:

ApiQuery(documentFilterQuery(SomeFilterQuery))

Where documentFilterQuery is a function that takes the class (or an instance of it) and returns a documentation object that contains the properties.
Something along these lines:

export const documentFilterQuery(FilterQuery: new () => object) {
  const filterQuery = new FilterQuery();
  const properties = Object.keys(filterQuery);
  // ...
}

Of course, the class won't contain the properties at runtime due to JavaScript being JavaScript, and now I'm looking for an elegant way around that. I noticed that all the objects that are being validated by class-validator via the one of the validation decorators will contain all properties, even optional and undefined ones and I was wondering how class-validator was doing this. Maybe someone has an idea?

#

I found this answer on stackoverflow and it was quite helpful! class-validator is indeed very helpful in this, but I needed to use the decorators themselves. https://stackoverflow.com/a/70553817

This is how my solution looks like:

export function documentFilterQuery(
  FilterQuery: new () => object
): ApiQueryOptions {
  const properties = getFilterQueryProperties(FilterQuery);
  const description = `Can be used to filter for the resource's properties. Filter syntax follows the JSON:API specification: https://jsonapi.org/recommendations/#filtering
  
  The following properties are supported: ${Object.keys(properties).join(',')}`;
  const result: ApiQueryOptions = {
    name: 'filter',
    description,
    type: String,
    required: false,
  };
  return result;
}

function getFilterQueryProperties(FilterQuery: new () => object) {
  const metadataStorage = getMetadataStorage();
  const targetMetadata = metadataStorage.getTargetValidationMetadatas(
    FilterQuery,
    undefined,
    false,
    false,
  );
  const groupedMetadata = metadataStorage.groupByPropertyName(targetMetadata);
  return Object.fromEntries(
    Object.entries(groupedMetadata).map(([property, decorators]) => {
      const CM = decorators.map((decorator) =>
        metadataStorage
          .getTargetValidatorConstraints(decorator.constraintCls)
          .map((v) => v.name),
      );
      return [property, CM.flat()];
    }),
  );
}

In the controller, this can be used like so:

@ApiQuery(documentFilterQuery(SomeFilterQuery))
grave wharf
#

Ah well, that's only almost it! The issue is that the parameter key is still dynamic (e.g. filter[foo]=bar), but Swagger will only be able to do filter=bar with my code above. It appears that OpenApi allows objects as types for query parameters, but the ApiQueryOptions does not know the additionalProperties option. 🤔