178 lines
5.0 KiB
TypeScript
178 lines
5.0 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import {
|
|
AttributeDefinitionEntity,
|
|
AttributeType,
|
|
} from '../../entities/attribute-definition.entity';
|
|
import { BusinessAttributeValueEntity } from '../../entities/business-attribute-value.entity';
|
|
import { BusinessEntity } from '../../entities/business.entity';
|
|
import { CreateBusinessAttributeValueDto } from './dto/create-business-attribute-value.dto';
|
|
import { UpdateBusinessAttributeValueDto } from './dto/update-business-attribute-value.dto';
|
|
|
|
@Injectable()
|
|
export class BusinessAttributeValuesService {
|
|
constructor(
|
|
@InjectRepository(BusinessAttributeValueEntity)
|
|
private readonly repo: Repository<BusinessAttributeValueEntity>,
|
|
@InjectRepository(BusinessEntity)
|
|
private readonly businessRepo: Repository<BusinessEntity>,
|
|
@InjectRepository(AttributeDefinitionEntity)
|
|
private readonly defRepo: Repository<AttributeDefinitionEntity>,
|
|
) {}
|
|
|
|
async findAll(businessId?: string): Promise<BusinessAttributeValueEntity[]> {
|
|
const qb = this.repo
|
|
.createQueryBuilder('v')
|
|
.leftJoinAndSelect('v.business', 'business')
|
|
.leftJoinAndSelect('v.attributeDefinition', 'def')
|
|
.orderBy('v.createdAt', 'DESC');
|
|
|
|
if (businessId) {
|
|
qb.andWhere('v.businessId = :businessId', { businessId });
|
|
}
|
|
|
|
return qb.getMany();
|
|
}
|
|
|
|
async findOne(id: string): Promise<BusinessAttributeValueEntity> {
|
|
const row = await this.repo.findOne({
|
|
where: { id },
|
|
relations: { business: true, attributeDefinition: true },
|
|
});
|
|
if (!row) {
|
|
throw new NotFoundException('Business attribute value not found');
|
|
}
|
|
return row;
|
|
}
|
|
|
|
async create(
|
|
dto: CreateBusinessAttributeValueDto,
|
|
): Promise<BusinessAttributeValueEntity> {
|
|
const business = await this.businessRepo.findOne({
|
|
where: { id: dto.businessId },
|
|
});
|
|
if (!business) {
|
|
throw new NotFoundException('Business not found');
|
|
}
|
|
|
|
const def = await this.defRepo.findOne({
|
|
where: { id: dto.attributeDefinitionId },
|
|
});
|
|
if (!def) {
|
|
throw new NotFoundException('Attribute definition not found');
|
|
}
|
|
|
|
if (def.categoryId !== business.categoryId) {
|
|
throw new BadRequestException(
|
|
'Attribute definition must belong to the same category as the business',
|
|
);
|
|
}
|
|
|
|
const dup = await this.repo.findOne({
|
|
where: {
|
|
businessId: dto.businessId,
|
|
attributeDefinitionId: dto.attributeDefinitionId,
|
|
},
|
|
});
|
|
if (dup) {
|
|
throw new ConflictException(
|
|
'This attribute is already set for this business; use PATCH instead',
|
|
);
|
|
}
|
|
|
|
const normalized = this.normalizeAndValidateValue(def, dto.value);
|
|
|
|
const entity = this.repo.create({
|
|
businessId: dto.businessId,
|
|
attributeDefinitionId: dto.attributeDefinitionId,
|
|
value: normalized,
|
|
});
|
|
|
|
return this.repo.save(entity);
|
|
}
|
|
|
|
async update(
|
|
id: string,
|
|
dto: UpdateBusinessAttributeValueDto,
|
|
): Promise<BusinessAttributeValueEntity> {
|
|
const row = await this.findOne(id);
|
|
|
|
const def =
|
|
row.attributeDefinition ??
|
|
(await this.defRepo.findOne({
|
|
where: { id: row.attributeDefinitionId },
|
|
}));
|
|
|
|
if (!def) {
|
|
throw new NotFoundException('Attribute definition not found');
|
|
}
|
|
|
|
if (dto.value !== undefined) {
|
|
row.value = this.normalizeAndValidateValue(def, dto.value);
|
|
}
|
|
|
|
return this.repo.save(row);
|
|
}
|
|
|
|
async remove(id: string): Promise<void> {
|
|
const result = await this.repo.delete(id);
|
|
if (!result.affected) {
|
|
throw new NotFoundException('Business attribute value not found');
|
|
}
|
|
}
|
|
|
|
private normalizeAndValidateValue(
|
|
def: AttributeDefinitionEntity,
|
|
raw: string | null | undefined,
|
|
): string | null {
|
|
const empty =
|
|
raw === undefined || raw === null || String(raw).trim() === '';
|
|
|
|
if (empty) {
|
|
if (def.required) {
|
|
throw new BadRequestException(`value required for attribute "${def.key}"`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const s = String(raw).trim();
|
|
|
|
switch (def.type) {
|
|
case AttributeType.BOOLEAN: {
|
|
const low = s.toLowerCase();
|
|
if (!['true', 'false', '1', '0'].includes(low)) {
|
|
throw new BadRequestException(
|
|
`invalid boolean for "${def.key}" (use true/false)`,
|
|
);
|
|
}
|
|
return low === 'true' || low === '1' ? 'true' : 'false';
|
|
}
|
|
case AttributeType.NUMBER: {
|
|
const n = Number(s);
|
|
if (!Number.isFinite(n)) {
|
|
throw new BadRequestException(`invalid number for "${def.key}"`);
|
|
}
|
|
return String(n);
|
|
}
|
|
case AttributeType.SELECT: {
|
|
const opts = def.options ?? [];
|
|
if (!opts.includes(s)) {
|
|
throw new BadRequestException(
|
|
`value must be one of options for "${def.key}"`,
|
|
);
|
|
}
|
|
return s;
|
|
}
|
|
case AttributeType.TEXT:
|
|
default:
|
|
return s;
|
|
}
|
|
}
|
|
}
|