From 70e7419b78fc75daf5dc87fbd39c9c3577fcfd6b Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 19 Mar 2026 14:24:59 +0100 Subject: [PATCH 1/2] Added application metadata --- src/entities/application.entity.ts | 3 +++ src/entities/dto/create-application.dto.ts | 6 ++++++ .../1773923548971-add-application-metadata.ts | 14 ++++++++++++++ .../device-management/application.service.ts | 1 + 4 files changed, 24 insertions(+) create mode 100644 src/migration/1773923548971-add-application-metadata.ts diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index 1033fdef..da5042f9 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -116,4 +116,7 @@ export class Application extends DbBaseEntity { @Column({ nullable: true }) chirpstackId?: string; + + @Column({ type: "jsonb", nullable: true }) + metadata: JSON; } diff --git a/src/entities/dto/create-application.dto.ts b/src/entities/dto/create-application.dto.ts index 9026a3c4..d1722bf6 100644 --- a/src/entities/dto/create-application.dto.ts +++ b/src/entities/dto/create-application.dto.ts @@ -7,6 +7,7 @@ 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 { IsMetadataJson } from "@helpers/is-metadata-json.validator"; export class CreateApplicationDto { @ApiProperty({ required: true }) @@ -96,4 +97,9 @@ export class CreateApplicationDto { @IsString() @MaxLength(1024) chirpstackId?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsMetadataJson(nameof("metadata")) + metadata?: JSON; } diff --git a/src/migration/1773923548971-add-application-metadata.ts b/src/migration/1773923548971-add-application-metadata.ts new file mode 100644 index 00000000..bd3017a5 --- /dev/null +++ b/src/migration/1773923548971-add-application-metadata.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddApplicationMetadata1773923548971 implements MigrationInterface { + name = 'AddApplicationMetadata1773923548971' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "application" ADD "metadata" jsonb`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "application" DROP COLUMN "metadata"`); + } + +} diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index 49d9a482..287c39c7 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -640,6 +640,7 @@ export class ApplicationService { application.personalData = applicationDto.personalData; application.hardware = applicationDto.hardware; application.permissions = await this.permissionService.findManyByIds(applicationDto.permissionIds); + application.metadata = applicationDto.metadata; // Set metadata dependencies application.controlledProperties = applicationDto.controlledProperties From f6ade8b5c04965ce4ecd75cf9cbd5b31633e9096 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 19 Mar 2026 15:46:17 +0100 Subject: [PATCH 2/2] Stored application metadata as real JSON --- src/entities/dto/create-application.dto.ts | 4 +- .../is-metadata-json-object.validator.ts | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/helpers/is-metadata-json-object.validator.ts diff --git a/src/entities/dto/create-application.dto.ts b/src/entities/dto/create-application.dto.ts index d1722bf6..a460b882 100644 --- a/src/entities/dto/create-application.dto.ts +++ b/src/entities/dto/create-application.dto.ts @@ -7,7 +7,7 @@ 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 { IsMetadataJson } from "@helpers/is-metadata-json.validator"; +import { IsMetadataJsonObject } from "@helpers/is-metadata-json-object.validator"; export class CreateApplicationDto { @ApiProperty({ required: true }) @@ -100,6 +100,6 @@ export class CreateApplicationDto { @ApiProperty({ required: false }) @IsOptional() - @IsMetadataJson(nameof("metadata")) + @IsMetadataJsonObject(nameof("metadata")) metadata?: JSON; } diff --git a/src/helpers/is-metadata-json-object.validator.ts b/src/helpers/is-metadata-json-object.validator.ts new file mode 100644 index 00000000..488bf8bb --- /dev/null +++ b/src/helpers/is-metadata-json-object.validator.ts @@ -0,0 +1,55 @@ +import { ErrorCodes } from "@enum/error-codes.enum"; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from "class-validator"; + +@ValidatorConstraint({ name: "isMetadataJsonObject", async: false }) +export class IsMetadataJsonObjectConstraint implements ValidatorConstraintInterface { + private message = ""; + + public validate(value: any, args: ValidationArguments): boolean { + const [propertyName] = args.constraints; + this.message = `${propertyName} must be an object with non-empty string keys and values`; + + if (typeof value !== "object") { + return false; + } + try { + for (const key of Object.keys(value)) { + if (typeof key !== "string" || key.trim() === "") { + this.message = ErrorCodes.InvalidKeyInKeyValuePair; + return false; + } + if (typeof value[key] !== "string" || value[key].trim() === "") { + this.message = ErrorCodes.InvalidValueInKeyValuePair; + return false; + } + } + } catch (error) { + return false; + } + + return true; + } + + public defaultMessage(_args: ValidationArguments): string { + return this.message; + } +} + +export function IsMetadataJsonObject(property: string, validationOptions?: ValidationOptions) { + return function (object: unknown, propertyName: string): void { + registerDecorator({ + name: "isMetadataJsonObject", + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: IsMetadataJsonObjectConstraint, + }); + }; +}