Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions sql-migration/application-contact-persons.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Migrate deprecated application contact information to contact persons.

-- -----------------------------------------------------------------------------

-- Create a contact person for each application having full contact information, i.e. name, email and phone.

INSERT INTO contact_person("applicationId", "name", "email", "phone")
SELECT id,
"contactPerson",
"contactEmail",
"contactPhone"
FROM application
WHERE COALESCE("contactPerson", '') != ''
AND COALESCE("contactEmail", '') != ''
AND COALESCE("contactPhone", '') != ''
;

-- Clear deprecated contact information (which has been migrated to contact persons).

UPDATE application
SET "contactPerson" = '',
"contactEmail" = '',
"contactPhone" = ''
FROM contact_person
WHERE contact_person."applicationId" = application.id
AND contact_person."name" = application."contactPerson"
AND contact_person."email" = application."contactEmail"
AND contact_person."phone" = application."contactPhone"
;
7 changes: 7 additions & 0 deletions src/entities/application.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Unique,
} from "typeorm";
import { ApplicationDeviceType } from "./application-device-type.entity";
import { ContactPerson } from "./contact-person.entity";
import { ControlledProperty } from "./controlled-property.entity";
import { Multicast } from "./multicast.entity";
import { Permission } from "./permissions/permission.entity";
Expand Down Expand Up @@ -96,6 +97,12 @@ export class Application extends DbBaseEntity {
@Column({ nullable: true })
contactPhone?: string;

@OneToMany(() => ContactPerson, entity => entity.application, {
nullable: true,
cascade: true,
})
contactPersons?: ContactPerson[];

@Column({ nullable: true })
personalData?: boolean;

Expand Down
25 changes: 25 additions & 0 deletions src/entities/contact-person.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { Application } from "./application.entity";
import { DbBaseEntity } from "./base.entity";

@Entity("contact_person")
export class ContactPerson extends DbBaseEntity {
@Column({nullable: true})
role: string;

@Column()
name: string;

@Column()
email: string;

@Column()
phone: string;

@ManyToOne(() => Application, application => application.contactPersons, {
onDelete: "CASCADE",
// Delete the row instead of null'ing application. Useful for updates
orphanedRowAction: "delete",
})
application: Application;
}
9 changes: 8 additions & 1 deletion src/entities/dto/create-application.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { IsSwaggerOptional } from "@helpers/optional-validator";
import { IsPhoneNumberString } from "@helpers/phone-number.validator";
import { nameof } from "@helpers/type-helper";
import { ApiProperty } from "@nestjs/swagger";
import { ArrayUnique, IsArray, IsBoolean, IsEnum, IsOptional, IsString, MaxLength, MinLength } from "class-validator";
import { ArrayUnique, IsArray, IsBoolean, IsEnum, IsOptional, IsString, MaxLength, MinLength, ValidateNested } from "class-validator";
import { CreateContactPersonDto } from "@dto/create-contact-person.dto";
import { Type } from "class-transformer";

export class CreateApplicationDto {
@ApiProperty({ required: true })
Expand Down Expand Up @@ -61,6 +63,11 @@ export class CreateApplicationDto {
@MaxLength(12)
contactPhone?: string;

@IsSwaggerOptional()
@ValidateNested()
@Type(() => CreateContactPersonDto)
contactPersons: CreateContactPersonDto[];

@IsSwaggerOptional()
@IsBoolean()
personalData?: boolean;
Expand Down
31 changes: 31 additions & 0 deletions src/entities/dto/create-contact-person.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength, MinLength } from "class-validator";
import { IsPhoneNumberString } from "@helpers/phone-number.validator";
import { nameof } from "@helpers/type-helper";

export class CreateContactPersonDto {
@IsOptional()
@IsNumber()
id?: number;

@IsString()
@MaxLength(100)
role?: string;

@ApiProperty({ required: true })
@IsString()
@IsNotEmpty()
@MaxLength(100)
name: string;

@ApiProperty({ required: true })
@IsString()
@IsEmail()
@MaxLength(100)
email?: string;

@ApiProperty({ required: true })
@IsPhoneNumberString(nameof<CreateContactPersonDto>("phone"))
@MaxLength(12)
phone?: string;
}
20 changes: 20 additions & 0 deletions src/migration/1773473328308-added-application-contact-persons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddedApplicationContactPersons1773473328308 implements MigrationInterface {
name = 'AddedApplicationContactPersons1773473328308'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "contact_person" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "role" character varying, "name" character varying NOT NULL, "email" character varying NOT NULL, "phone" character varying NOT NULL, "createdById" integer, "updatedById" integer, "applicationId" integer, CONSTRAINT "PK_12d9c34f76290c4e2ad2aa5e33f" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "contact_person" ADD CONSTRAINT "FK_e45f40c60ec5ed72ba1fabae2a2" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "contact_person" ADD CONSTRAINT "FK_bb8fef259a0f71539e39aece219" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "contact_person" ADD CONSTRAINT "FK_fd89e3a2a1cff048fbc66e536fb" FOREIGN KEY ("applicationId") REFERENCES "application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "contact_person" DROP CONSTRAINT "FK_fd89e3a2a1cff048fbc66e536fb"`);
await queryRunner.query(`ALTER TABLE "contact_person" DROP CONSTRAINT "FK_bb8fef259a0f71539e39aece219"`);
await queryRunner.query(`ALTER TABLE "contact_person" DROP CONSTRAINT "FK_e45f40c60ec5ed72ba1fabae2a2"`);
await queryRunner.query(`DROP TABLE "contact_person"`);
}

}
2 changes: 2 additions & 0 deletions src/modules/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { SigFoxDevice } from "@entities/sigfox-device.entity";
import { SigFoxGroup } from "@entities/sigfox-group.entity";
import { User } from "@entities/user.entity";
import { AuditLog } from "@services/audit-log.service";
import { ContactPerson } from "@entities/contact-person.entity";

@Module({
imports: [
Expand Down Expand Up @@ -73,6 +74,7 @@ import { AuditLog } from "@services/audit-log.service";
MQTTExternalBrokerDevice,
Gateway,
Downlink,
ContactPerson,
]),
],
providers: [AuditLog],
Expand Down
18 changes: 17 additions & 1 deletion src/services/device-management/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { DataTargetService } from "@services/data-targets/data-target.service";
import { OrganizationService } from "@services/user-management/organization.service";
import { PermissionService } from "@services/user-management/permission.service";
import { Brackets, DeleteResult, In, Repository } from "typeorm";
import { ContactPerson } from "@entities/contact-person.entity";
import { CreateContactPersonDto } from "@dto/create-contact-person.dto";

@Injectable()
export class ApplicationService {
Expand Down Expand Up @@ -245,7 +247,7 @@ export class ApplicationService {
: { id: In(allowedApplications) },
take: query.limit,
skip: query.offset,
relations: ["iotDevices", nameof<Application>("belongsTo")],
relations: ["iotDevices", nameof<Application>("contactPersons"), nameof<Application>("belongsTo")],
order: { id: query.sort },
});

Expand Down Expand Up @@ -346,6 +348,7 @@ export class ApplicationService {
relations: [
"iotDevices",
"belongsTo",
nameof<Application>("contactPersons"),
nameof<Application>("controlledProperties"),
nameof<Application>("deviceTypes"),
"permissions",
Expand Down Expand Up @@ -395,6 +398,7 @@ export class ApplicationService {
nameof<Application>("dataTargets"),
nameof<Application>("controlledProperties"),
nameof<Application>("deviceTypes"),
nameof<Application>("contactPersons"),
],
});

Expand Down Expand Up @@ -641,6 +645,18 @@ export class ApplicationService {
application.hardware = applicationDto.hardware;
application.permissions = await this.permissionService.findManyByIds(applicationDto.permissionIds);

application.contactPersons =
applicationDto.contactPersons?.map((dto: CreateContactPersonDto): ContactPerson => {
const contactPerson = new ContactPerson();
contactPerson.id = dto.id;
contactPerson.name = dto.name;
contactPerson.phone = dto.phone;
contactPerson.email = dto.email;
contactPerson.role = dto.role;

return contactPerson;
}) ?? [];

// Set metadata dependencies
application.controlledProperties = applicationDto.controlledProperties
? this.buildControlledPropertyDeviceType(
Expand Down