3

I have a Dto that I want to enable the service layer to filter: The method selectFields takes an array of field names that should be returned, the other properties will be removed.
What is a short way to enumerate the properties on the class so I can loop through them and set the filtered ones to null?
In the BaseDto I take care of cleaning falsy values (well I need the same function here too as a matter of fact).

class UserServiceDto extends BaseDto {
  constructor(userDto) {
    super();
    this.fbUserId = userDto.fbUserId;
    this.fbFirstName = userDto.fbFirstName;
    this.fbLastName = userDto.fbLastName;
    this.gender = userDto.gender;
    this.birthdate = userDto.birthdate;
    this.aboutMe = userDto.aboutMe;
    this.deviceToken = userDto.deviceToken;
    this.refreshToken = userDto.refreshToken;
    this.updatedAt = userDto.updatedAt;
    this.createdAt = userDto.createdAt;
  }

  selectFields(fields) {
    // --> what's your take? 
  }

  toJson() {
    return super.toJson();
  }
}

Edit:
The service layer receives a dto from repository layer including all database fields. The ServiceLayerDto aims at filtering out fields that are not required by the web api (or should not be exposed as a security measure e.g. PK field, isDeleted, etc). So the result would I'm looking at the end of a service method for would look something like:

return new UserServiceDto(userDto)
  .selectFields('fbUserId', 'fbFirstName', 'fbLastName', 'birthdate', 'aboutMe', 'updatedAt', 'createdAt')
  .toJson();

The return value would be a plain json object that the web layer (controller) sends back to the http client.

dhilt
  • 13,532
  • 6
  • 48
  • 67
html_programmer
  • 14,612
  • 12
  • 59
  • 125

2 Answers2

2

If you are ok with spread operator, you may try following approach:

class UserServiceDto {
  constructor() {
    this.a = 1;
    this.b = 2;
    this.c = 3;
  }
  selectFields(...fields) {
    const result = {};
    fields.forEach(key => result[key] = this[key]);
    return result;
  }
}

new UserServiceDto().selectFields('a', 'c'); // {a: 1, c: 3}

Looking to super.toJson() call, I think that it would not work due to the result of my selectFields() call would not be an instance of UserServiceDto class. There are some possible ways from this point I see:

  • instantiate new UserServiceDto object inside selectFields() body, remove all fields that not listed in the ...fields array (javascript delete is okey) and return it;
  • play with UserServiceDto constructor params to save positive logic on selectFields(), and pass to constructor only that props that need to be set up; in this case instantiating a temporary object will not require properties removing;
  • change the signature of toJson method, or better add a new signature, which would allow to pass fields array and then put current selectFields logic inside toJson method (and remove selectFields method at all): new UserServiceDto().toJson('a', 'c')...
dhilt
  • 13,532
  • 6
  • 48
  • 67
  • Thanks, the approaches are all interesting, seems like it comes down to having the equivalent of a limited 'view' that the web layer can call on the service layer (whereas the repository would have the 'table'). I'm going to accept your answer for this use case, but I'll see if I can find a more solid solution that can fit the overall app architecture (i.e. specifying and returning named 'views' rather than passing dtos with individual field filtering for every service method). – html_programmer Dec 13 '17 at 09:37
0

Purely for info, I ultimately changed my app architecture.

The repository returns a Dto to the service layer (dto being mapped directly from the sql queries).
The service builds a static View based on the Dto and returns it to the web layer (represented by a plain json object).

In my directory structure, I have:

- service
-- views
--- index.js
--- UserInfo.js

The view is a simple filter. E.g. UserInfoView:

exports.build = ({ fbUserId, fbFirstName, fbLastName, gender, birthdate, aboutMe, updatedAt, createdAt }) => {
  return {
    fbUserId,
    fbFirstName,
    fbLastName,
    gender,
    birthdate,
    aboutMe,
    updatedAt,
    createdAt,
  };
};

Using the view, e.g. UserInfoView in the service looks like this:

const Views = require('../service/views');

exports.findActiveByUserId = async (pUserId) => {
  const userDto = await UserRepository.findActiveByUserId(pUserId);
  if (!userDto) {
    throw new ServiceError(Err.USER_NOT_FOUND, Err.USER_NOT_FOUND_MSG);
  }
  return Views.UserInfo.build(userDto.toJson());
};

I think that this is much more descriptive compared to my initial take on the problem. Also, it keeps the data objects plain (no additional methods required).
It is unfortunate that I can't require receiving an type (View) in the web layer, I might be able to solve that problem with Typescript later on.

html_programmer
  • 14,612
  • 12
  • 59
  • 125