Ritik Jain
Ritik Jain

Reputation: 68

How do I make a model property unique in loopback 4 mongoDB?

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);
  }
}

Upvotes: 4

Views: 1096

Answers (1)

Ashwin Soni
Ashwin Soni

Reputation: 183

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);
  }
} 

Upvotes: 2

Related Questions