4

I want to make the name field unique in loopback 4 models. I am using MongoDB datasource. I tried using index: { unique: true } which does not seem to work. I read loopback documentation about validatesUniquenessOf() validation but I do not clearly understand how and where to use it.

@model()
export class BillingAddress extends Entity {
  @property({
    type: 'string',
    id: true,
    mongodb: {dataType: 'ObjectId'}
  })
  _id: string;

  @property({
    type: 'string',
    required: true,
    index: {
        unique: true
    },
  })
  name: string;
  ...
  ...
  ...
  constructor(data?: Partial<BillingAddress>) {
    super(data);
  }
}
Ritik Jain
  • 68
  • 1
  • 4

1 Answers1

2

validatesUniquenessOf() was valid and available in older loopback versions https://apidocs.strongloop.com/loopback-datasource-juggler/#validatable-validatesuniquenessof

In Loopback 4, as specified in documentation, there are two ways to handle such unique constrainsts,

1. Adding validation at ORM layer - https://loopback.io/doc/en/lb4/Validation-ORM-layer.html

Schema constraints are enforced by specific databases, such as unique index

So we need to add the unique contraints at db level incase of mongodb

> db.BillingAddress.createIndex({ "name": 1 }, { unique: true })
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}

Once the unique index is created then when we try inserting the billing address with the same name we should get the below error,

MongoError: E11000 duplicate key error collection: BillingAddress.BillingAddress index: name_1 dup key: { : "Bob" }

Request POST /billing-addresses failed with status code 500. 

2. Adding validation at Controller layer - https://loopback.io/doc/en/lb4/Validation-controller-repo-service-layer.html

i. validate function in controller:

// create a validateUniqueBillingAddressName function and call it here
if (!this.validateUniqueBillingAddressName(name))
  throw new HttpErrors.UnprocessableEntity('Name already exist');
return this.billingAddressRepository.create(billingAddress);

ii. validate at interceptor and injecting it in a controller:

> lb4 interceptor
? Interceptor name: validateBillingAddressName
? Is it a global interceptor? No
   create src/interceptors/validate-billing-address-name.interceptor.ts
   update src/interceptors/index.ts

you may have to write a validation logic in your interceptor file validate-billing-address-name.interceptor.ts something like,

import {
  injectable,
  Interceptor,
  InvocationContext,
  InvocationResult,
  Provider,
  ValueOrPromise
} from '@loopback/core';
import {repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {BillingAddressRepository} from '../repositories';

/**
 * This class will be bound to the application as an `Interceptor` during
 * `boot`
 */
@injectable({tags: {key: ValidateBillingAddressNameInterceptor.BINDING_KEY}})
export class ValidateBillingAddressNameInterceptor implements Provider<Interceptor> {
  static readonly BINDING_KEY = `interceptors.${ValidateBillingAddressNameInterceptor.name}`;

  constructor(
    @repository(BillingAddressRepository)
    public billingAddressRepository: BillingAddressRepository
  ) { }

  /**
   * This method is used by LoopBack context to produce an interceptor function
   * for the binding.
   *
   * @returns An interceptor function
   */
  value() {
    return this.intercept.bind(this);
  }

  /**
   * The logic to intercept an invocation
   * @param invocationCtx - Invocation context
   * @param next - A function to invoke next interceptor or the target method
   */
  async intercept(
    invocationCtx: InvocationContext,
    next: () => ValueOrPromise<InvocationResult>,
  ) {
    try {
      // Add pre-invocation logic here
      if (invocationCtx.methodName === 'create') {
        const {name} = invocationCtx.args[0];
        const nameAlreadyExist = await this.billingAddressRepository.find({where: {name}})
        if (nameAlreadyExist.length) {
          throw new HttpErrors.UnprocessableEntity(
            'Name already exist',
          );
        }
      }
      const result = await next();
      // Add post-invocation logic here
      return result;
    } catch (err) {
      // Add error handling logic here
      throw err;
    }
  }
}

and then injecting this interceptor via its binding key to your billing-address.controller.ts file something like,

import {intercept} from '@loopback/context';
import {
  repository
} from '@loopback/repository';
import {
  getModelSchemaRef,
  post,

  requestBody
} from '@loopback/rest';
import {ValidateBillingAddressNameInterceptor} from '../interceptors';
import {BillingAddress} from '../models';
import {BillingAddressRepository} from '../repositories';

export class BillingAddressController {
  constructor(
    @repository(BillingAddressRepository)
    public billingAddressRepository: BillingAddressRepository,
  ) { }

  @intercept(ValidateBillingAddressNameInterceptor.BINDING_KEY)
  @post('/billing-addresses', {
    responses: {
      '200': {
        description: 'BillingAddress model instance',
        content: {'application/json': {schema: getModelSchemaRef(BillingAddress)}},
      },
    },
  })
  async create(
    @requestBody({
      content: {
        'application/json': {
          schema: getModelSchemaRef(BillingAddress, {
            title: 'NewBillingAddress',
            exclude: ['id'],
          }),
        },
      },
    })
    billingAddress: Omit<BillingAddress, 'id'>,
  ): Promise<BillingAddress> {
    return this.billingAddressRepository.create(billingAddress);
  }
} 
Ashwin Soni
  • 173
  • 1
  • 9
  • 1
    Is DB level the only way to add unique constraints for MongoDB? is there any workaround/another way in loopback 4? – Ritik Jain Mar 08 '21 at 16:06
  • yes, there are few other ways to do so. let me help you, updating my answer accordingly – Ashwin Soni Mar 08 '21 at 17:55
  • @RitikJain Please check i've added few more ways in answer to handle unique constraints. Let me know if that helps you solve your problem. Will try to improve it further if required – Ashwin Soni Mar 08 '21 at 18:26