mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-03 01:33:39 +00:00
customer registration api integration
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
|
||||
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
|
||||
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
|
||||
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
|
||||
import { DropdownSettingsService } from "./dropdown-settings.service";
|
||||
|
||||
@ApiTags("dropdown-settings")
|
||||
@Controller("dropdown-settings")
|
||||
export class DropdownSettingsController {
|
||||
constructor(private readonly service: DropdownSettingsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List all dropdown settings" })
|
||||
list() {
|
||||
return this.service.list();
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiOperation({ summary: "Get a dropdown setting by ID" })
|
||||
getById(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.service.getById(id);
|
||||
}
|
||||
|
||||
@Get("by-code/:code")
|
||||
@ApiOperation({ summary: "Get a dropdown setting by its stable code" })
|
||||
getByCode(@Param("code") code: string) {
|
||||
return this.service.getByCode(code);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new dropdown setting" })
|
||||
create(@Body() dto: CreateDropdownSettingDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a dropdown setting's metadata" })
|
||||
update(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateDropdownSettingDto,
|
||||
) {
|
||||
return this.service.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Soft-delete a dropdown setting" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||
return this.service.remove(id);
|
||||
}
|
||||
|
||||
/* ------------------------- option routes ------------------------- */
|
||||
|
||||
@Put(":id/options")
|
||||
@ApiOperation({ summary: "Replace the full option list for a setting" })
|
||||
replaceOptions(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() options: CreateDropdownOptionDto[],
|
||||
) {
|
||||
return this.service.replaceOptions(id, options);
|
||||
}
|
||||
|
||||
@Post(":id/options")
|
||||
@ApiOperation({ summary: "Append a single option to a setting" })
|
||||
addOption(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body() dto: CreateDropdownOptionDto,
|
||||
) {
|
||||
return this.service.addOption(id, dto);
|
||||
}
|
||||
|
||||
@Patch("options/:optionId")
|
||||
@ApiOperation({ summary: "Update a single option" })
|
||||
updateOption(
|
||||
@Param("optionId", ParseUUIDPipe) optionId: string,
|
||||
@Body() dto: UpdateDropdownOptionDto,
|
||||
) {
|
||||
return this.service.updateOption(optionId, dto);
|
||||
}
|
||||
|
||||
@Delete("options/:optionId")
|
||||
@ApiOperation({ summary: "Soft-delete a single option" })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeOption(@Param("optionId", ParseUUIDPipe) optionId: string) {
|
||||
return this.service.removeOption(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import { DropdownSettingsController } from "./dropdown-settings.controller";
|
||||
import { DropdownSettingsRepository } from "./dropdown-settings.repository";
|
||||
import { DropdownSettingsService } from "./dropdown-settings.service";
|
||||
import { DROPDOWN_SETTINGS_REPOSITORY } from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([DropdownSetting, DropdownOption])],
|
||||
controllers: [DropdownSettingsController],
|
||||
providers: [
|
||||
DropdownSettingsRepository,
|
||||
{
|
||||
provide: DROPDOWN_SETTINGS_REPOSITORY,
|
||||
useExisting: DropdownSettingsRepository,
|
||||
},
|
||||
DropdownSettingsService,
|
||||
],
|
||||
exports: [DropdownSettingsService],
|
||||
})
|
||||
export class DropdownSettingsModule {}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { BaseRepository } from "@edr/api-common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import type { IDropdownSettingsRepository } from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Injectable()
|
||||
export class DropdownSettingsRepository
|
||||
extends BaseRepository<DropdownSetting>
|
||||
implements IDropdownSettingsRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(DropdownSetting)
|
||||
repository: Repository<DropdownSetting>,
|
||||
@InjectRepository(DropdownOption)
|
||||
private readonly optionsRepository: Repository<DropdownOption>,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
findByCode(code: string): Promise<DropdownSetting | null> {
|
||||
return this.repository.findOne({
|
||||
where: { code },
|
||||
relations: { children: true },
|
||||
order: { children: { order: "ASC" } },
|
||||
});
|
||||
}
|
||||
|
||||
override findById(id: string): Promise<DropdownSetting | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id },
|
||||
relations: { children: true },
|
||||
order: { children: { order: "ASC" } },
|
||||
});
|
||||
}
|
||||
|
||||
override findAll(): Promise<DropdownSetting[]> {
|
||||
return this.repository.find({
|
||||
order: { label: "ASC", children: { order: "ASC" } },
|
||||
relations: { children: true },
|
||||
});
|
||||
}
|
||||
|
||||
async replaceOptions(
|
||||
settingId: string,
|
||||
options: Array<Partial<DropdownOption>>,
|
||||
): Promise<DropdownOption[]> {
|
||||
await this.optionsRepository.delete({ settingId });
|
||||
if (options.length === 0) return [];
|
||||
const entities = options.map((o, idx) =>
|
||||
this.optionsRepository.create({
|
||||
...o,
|
||||
settingId,
|
||||
order: o.order ?? idx + 1,
|
||||
}),
|
||||
);
|
||||
return this.optionsRepository.save(entities);
|
||||
}
|
||||
|
||||
async addOption(
|
||||
settingId: string,
|
||||
option: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption> {
|
||||
const entity = this.optionsRepository.create({ ...option, settingId });
|
||||
return this.optionsRepository.save(entity);
|
||||
}
|
||||
|
||||
async updateOption(
|
||||
optionId: string,
|
||||
data: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption | null> {
|
||||
await this.optionsRepository.update(optionId, data as never);
|
||||
return this.optionsRepository.findOne({ where: { id: optionId } });
|
||||
}
|
||||
|
||||
async removeOption(optionId: string): Promise<void> {
|
||||
await this.optionsRepository.softDelete(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
|
||||
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
|
||||
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
|
||||
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
|
||||
import { DropdownOption } from "./entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "./entities/dropdown-setting.entity";
|
||||
import {
|
||||
DROPDOWN_SETTINGS_REPOSITORY,
|
||||
IDropdownSettingsRepository,
|
||||
} from "./interfaces/dropdown-settings.repository.interface";
|
||||
|
||||
@Injectable()
|
||||
export class DropdownSettingsService {
|
||||
constructor(
|
||||
@Inject(DROPDOWN_SETTINGS_REPOSITORY)
|
||||
private readonly repository: IDropdownSettingsRepository,
|
||||
) {}
|
||||
|
||||
list(): Promise<DropdownSetting[]> {
|
||||
return this.repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<DropdownSetting> {
|
||||
const setting = await this.repository.findById(id);
|
||||
if (!setting) throw new NotFoundException(`Setting ${id} not found`);
|
||||
return setting;
|
||||
}
|
||||
|
||||
async getByCode(code: string): Promise<DropdownSetting> {
|
||||
const setting = await this.repository.findByCode(code);
|
||||
if (!setting) throw new NotFoundException(`Setting "${code}" not found`);
|
||||
return setting;
|
||||
}
|
||||
|
||||
async create(dto: CreateDropdownSettingDto): Promise<DropdownSetting> {
|
||||
const existing = await this.repository.findByCode(dto.code);
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Dropdown setting with code "${dto.code}" already exists`,
|
||||
);
|
||||
}
|
||||
|
||||
const setting = await this.repository.create({
|
||||
code: dto.code,
|
||||
label: dto.label,
|
||||
description: dto.description ?? null,
|
||||
multiple: dto.multiple ?? false,
|
||||
meta: dto.meta ?? null,
|
||||
});
|
||||
|
||||
if (dto.children && dto.children.length > 0) {
|
||||
await this.repository.replaceOptions(setting.id, dto.children);
|
||||
}
|
||||
|
||||
return this.getById(setting.id);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateDropdownSettingDto,
|
||||
): Promise<DropdownSetting> {
|
||||
await this.getById(id);
|
||||
const updated = await this.repository.update(id, dto);
|
||||
if (!updated) throw new NotFoundException(`Setting ${id} not found`);
|
||||
return this.getById(id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.getById(id);
|
||||
await this.repository.softDelete(id);
|
||||
}
|
||||
|
||||
/* ------------------------ option operations ------------------------ */
|
||||
|
||||
async replaceOptions(
|
||||
settingId: string,
|
||||
options: CreateDropdownOptionDto[],
|
||||
): Promise<DropdownOption[]> {
|
||||
await this.getById(settingId);
|
||||
return this.repository.replaceOptions(settingId, options);
|
||||
}
|
||||
|
||||
async addOption(
|
||||
settingId: string,
|
||||
dto: CreateDropdownOptionDto,
|
||||
): Promise<DropdownOption> {
|
||||
await this.getById(settingId);
|
||||
return this.repository.addOption(settingId, dto);
|
||||
}
|
||||
|
||||
async updateOption(
|
||||
optionId: string,
|
||||
dto: UpdateDropdownOptionDto,
|
||||
): Promise<DropdownOption> {
|
||||
const updated = await this.repository.updateOption(optionId, dto);
|
||||
if (!updated) throw new NotFoundException(`Option ${optionId} not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async removeOption(optionId: string): Promise<void> {
|
||||
await this.repository.removeOption(optionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { DropdownOptionMetaDto } from "./dropdown-option-meta.dto";
|
||||
|
||||
export class CreateDropdownOptionDto {
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
disabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
order?: number;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DropdownOptionMetaDto)
|
||||
meta?: DropdownOptionMetaDto;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./create-dropdown-option.dto";
|
||||
import { DropdownSettingMetaDto } from "./dropdown-setting-meta.dto";
|
||||
|
||||
export class CreateDropdownSettingDto {
|
||||
@IsString()
|
||||
@MaxLength(128)
|
||||
@Matches(/^[a-z][a-z0-9_]*$/i, {
|
||||
message: "code must be snake_case-friendly (letters, digits, underscores)",
|
||||
})
|
||||
code!: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
multiple?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DropdownSettingMetaDto)
|
||||
meta?: DropdownSettingMetaDto;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateDropdownOptionDto)
|
||||
children?: CreateDropdownOptionDto[];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IsOptional, IsString, MaxLength } from "class-validator";
|
||||
|
||||
export class DropdownOptionMetaDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
badge?: string;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Transform } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
|
||||
export class DropdownSettingMetaDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
searchable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
clearable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) =>
|
||||
Array.isArray(value)
|
||||
? (value as string[]).map((s) => s.trim()).filter(Boolean)
|
||||
: value,
|
||||
)
|
||||
permissions?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(32)
|
||||
version?: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownOptionDto } from "./create-dropdown-option.dto";
|
||||
|
||||
export class UpdateDropdownOptionDto extends PartialType(
|
||||
CreateDropdownOptionDto,
|
||||
) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateDropdownSettingDto } from "./create-dropdown-setting.dto";
|
||||
|
||||
export class UpdateDropdownSettingDto extends PartialType(
|
||||
OmitType(CreateDropdownSettingDto, ["children"] as const),
|
||||
) {}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
import { DropdownSetting } from "./dropdown-setting.entity";
|
||||
|
||||
export interface DropdownOptionMeta {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
@Entity({ name: "dropdown_options" })
|
||||
@Index(["settingId", "value"], { unique: true })
|
||||
export class DropdownOption extends BaseEntity {
|
||||
@ManyToOne(() => DropdownSetting, (setting) => setting.children, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "setting_id" })
|
||||
setting!: DropdownSetting;
|
||||
|
||||
@Column({ name: "setting_id", type: "uuid" })
|
||||
settingId!: string;
|
||||
|
||||
@Column({ name: "value", type: "varchar", length: 256 })
|
||||
value!: string;
|
||||
|
||||
@Column({ name: "label", type: "varchar", length: 256 })
|
||||
label!: string;
|
||||
|
||||
@Column({ name: "note", type: "text", nullable: true })
|
||||
note?: string | null;
|
||||
|
||||
@Column({ name: "is_disabled", type: "boolean", default: false })
|
||||
disabled!: boolean;
|
||||
|
||||
@Column({ name: "display_order", type: "integer", default: 0 })
|
||||
order!: number;
|
||||
|
||||
@Column({ name: "meta", type: "jsonb", nullable: true })
|
||||
meta?: DropdownOptionMeta | null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Column, Entity, Index, OneToMany } from "typeorm";
|
||||
|
||||
import { DropdownOption } from "./dropdown-option.entity";
|
||||
|
||||
export interface DropdownSettingMeta {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
searchable?: boolean;
|
||||
clearable?: boolean;
|
||||
permissions?: string[];
|
||||
version?: string;
|
||||
}
|
||||
|
||||
@Entity({ name: "dropdown_settings" })
|
||||
@Index(["code"], { unique: true })
|
||||
export class DropdownSetting extends BaseEntity {
|
||||
@Column({ name: "code", type: "varchar", length: 128, unique: true })
|
||||
code!: string;
|
||||
|
||||
@Column({ name: "label", type: "varchar", length: 256 })
|
||||
label!: string;
|
||||
|
||||
@Column({ name: "description", type: "text", nullable: true })
|
||||
description?: string | null;
|
||||
|
||||
@Column({ name: "multiple", type: "boolean", default: false })
|
||||
multiple!: boolean;
|
||||
|
||||
@Column({ name: "meta", type: "jsonb", nullable: true })
|
||||
meta?: DropdownSettingMeta | null;
|
||||
|
||||
@OneToMany(() => DropdownOption, (option) => option.setting, {
|
||||
cascade: true,
|
||||
})
|
||||
children!: DropdownOption[];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DropdownOption } from "../entities/dropdown-option.entity";
|
||||
import { DropdownSetting } from "../entities/dropdown-setting.entity";
|
||||
|
||||
/**
|
||||
* Contract every DropdownSettings repository must satisfy. Lets services
|
||||
* depend on the abstraction and lets tests swap in an in-memory fake.
|
||||
*/
|
||||
export const DROPDOWN_SETTINGS_REPOSITORY = Symbol(
|
||||
"DROPDOWN_SETTINGS_REPOSITORY",
|
||||
);
|
||||
|
||||
export interface IDropdownSettingsRepository {
|
||||
findAll(): Promise<DropdownSetting[]>;
|
||||
findById(id: string): Promise<DropdownSetting | null>;
|
||||
findByCode(code: string): Promise<DropdownSetting | null>;
|
||||
|
||||
create(data: Partial<DropdownSetting>): Promise<DropdownSetting>;
|
||||
update(
|
||||
id: string,
|
||||
data: Partial<DropdownSetting>,
|
||||
): Promise<DropdownSetting | null>;
|
||||
softDelete(id: string): Promise<void>;
|
||||
|
||||
/* Option-level helpers */
|
||||
replaceOptions(
|
||||
settingId: string,
|
||||
options: Array<Partial<DropdownOption>>,
|
||||
): Promise<DropdownOption[]>;
|
||||
addOption(
|
||||
settingId: string,
|
||||
option: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption>;
|
||||
updateOption(
|
||||
optionId: string,
|
||||
data: Partial<DropdownOption>,
|
||||
): Promise<DropdownOption | null>;
|
||||
removeOption(optionId: string): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user