Reputation: 68
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
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