Search code examples
node.jsloopbackjsdto

How to setup a separate DTO other than Persistable Model in Loopback 4


Consider this lb4 model

@model({
  name: 'users'
})
export class User extends Entity {
  @property({
    type: 'number',
    id: true,
  })
  id: number;

  @property({
    type: 'string',
    required: true,
  })
  first_name: string;

  @property({
    type: 'string',
  })
  middle_name?: string;

  @property({
    type: 'string',
  })
  last_name?: string;

  @property({
    type: 'string',
    required: true,
  })
  username: string;

  @property({
    type: 'string',
  })
  email?: string;

  @property({
    type: 'string',
  })
  phone?: string;

  @property({
    type: 'string',
    required: true,
  })
  password: string;

  @property({
    type: 'string',
  })
  external_id: string;

  @belongsTo(() => UserTenant)
  created_by: number;

  @belongsTo(() => UserTenant)
  modified_by: number;

  constructor(data?: Partial<User>) {
    super(data);
  }
}

Currently if we create a repository and controller for this model using lb4 cli, it will generate route methods CRUD with this same model as input/output. However, what we want is to have a separate DTO model (Non persisted to DB) being used as input/output DTO for controller, excluding properties password, created_by and modified_by. One way is to manually create such a model class and write down a converter class which will convert UserDTO object to User model above (copy individual properties). But this appears to be an overhead. Moreover, we want this to be done for many more models. So, doing it in this fashion doesn't seem to be right approach. Does lb4 provide any better way to achieve this ?


Solution

  • Apparently for the time being, there is now way to "hide" properties natively in LB4. Then I ended up modifying the Entity class with a new entity HideableEntity (extending Entity). In the HideableEntity, I modified the toJson() function like this:

    import {Entity, AnyObject} from '@loopback/repository';
    import {Options} from '@loopback/repository/src/common-types';
    
    export abstract class HideableEntity extends Entity {
      /**
       * Serialize into a plain JSON object
       */
      toJSON(): Object {
        const def = (<typeof HideableEntity>this.constructor).definition;
        if (def == null || def.settings.strict === false) {
          return this.toObject({ignoreUnknownProperties: false});
        }
    
        const json: AnyObject = {};
        for (const p in def.properties) {
          if (p in this) {
            json[p] = asJSON((this as AnyObject)[p]);
          }
        }
        return json;
      }
    
      /**
       * Convert to a plain object as DTO
       */
      toObject(options?: Options): Object {
        const def = (<typeof HideableEntity>this.constructor).definition;
    
        let obj: AnyObject;
        if (options && options.ignoreUnknownProperties === false) {
          obj = {};
          for (const p in this) {
            if (def != null && def.properties[p] && def.properties[p]['hide']) {
              continue;
            }
            let val = (this as AnyObject)[p];
            obj[p] = asObject(val, options);
          }
        } else {
          obj = this.toJSON();
        }
        return obj;
      }
    }
    
    function asJSON(value: any): any {
      if (value == null) return value;
      if (typeof value.toJSON === 'function') {
        return value.toJSON();
      }
      // Handle arrays
      if (Array.isArray(value)) {
        return value.map(item => asJSON(item));
      }
      return value;
    }
    
    function asObject(value: any, options?: Options): any {
      if (value == null) return value;
      if (typeof value.toObject === 'function') {
        return value.toObject(options);
      }
      if (typeof value.toJSON === 'function') {
        return value.toJSON();
      }
      if (Array.isArray(value)) {
        return value.map(item => asObject(item, options));
      }
      return value;
    }
    

    Then now in my Model extension HideableEntity, I add the property hide: true and it won't be added to the JSON output:

    export class User extends HideableEntity {
      @property({
        type: 'number',
        id: true,
        required: false,
      })
      id: number;
    
        @property({
            type: 'string',
            required: true,
        })
        email: string;
    
        @property({
            type: 'string',
            required: true,
            hide: true,
        })
        password: string;
    
        [...]
    

    In the case above, password will be hidden.