Skip to main content

Product Metadata

Overview

The Product Metadata feature allows administrators to define custom metadata structures for IoT products and their associated devices. This flexible system enables product-specific data collection without requiring schema changes.

Key Concept:

  • Products define the metadata structure (keys and data types)
  • Devices provide values for these predefined fields

This two-level approach ensures data consistency across all devices of the same product type while maintaining flexibility for different product categories.

Key Features

  • Template-based metadata: Define metadata structure once at the product level
  • Type safety: Support for string, number, boolean, and date types
  • Automatic validation: Frontend and backend validation based on defined types
  • Auto-cleanup: Obsolete metadata fields are automatically removed from devices when the product template changes
  • UI-driven configuration: No code changes required to add new metadata fields
  • Role-based access: Only superadmins can define product metadata structures

Architecture

High-Level Flow

┌─────────────────────────────────────────────────────────────┐
│ Product Level │
│ (Define Structure: keys + types) │
│ │
│ Product.metadata = { │
│ fields: [ │
│ { key: "temperature", type: "number" }, │
│ { key: "location", type: "string" }, │
│ { key: "isActive", type: "boolean" } │
│ ] │
│ } │
└─────────────────────────────────────────────────────────────┘

│ Devices inherit structure

┌─────────────────────────────────────────────────────────────┐
│ Device Level │
│ (Provide Values) │
│ │
│ Device.metadata = [ │
│ { key: "temperature", value: "25.5" }, │
│ { key: "location", value: "Building A" }, │
│ { key: "isActive", value: "true" } │
│ ] │
└─────────────────────────────────────────────────────────────┘

Component Architecture

Backend Components:

  • ProductController - Manages product metadata structure via PUT /product/:id
  • DeviceController - Manages device metadata values via PUT /device/:id/metadata
  • DeviceService.updateMetadata() - Business logic for metadata updates with auto-cleanup

Frontend Components:

  • ProductMetadataForm - Form to define product metadata structure
  • DeviceMetadataSheet - Drawer component containing device metadata form
  • DeviceMetadataForm - Form to edit device metadata values

Data Model

Product Metadata Structure

Stored in Product.metadata field as JSON:

interface ProductMetadata {
fields: Array<{
key: string; // Field identifier (e.g., "temperature")
type: string; // Data type: "string" | "number" | "boolean" | "date"
}>;
}

Example:

{
"fields": [
{ "key": "temperature", "type": "number" },
{ "key": "location", "type": "string" },
{ "key": "isActive", "type": "boolean" },
{ "key": "lastMaintenance", "type": "date" }
]
}

Device Metadata Values

Stored in Device.metadata field as JSON array:

interface DeviceMetadata {
key: string; // Must match a key from Product.metadata.fields
value: string; // All values stored as strings
}

Example:

[
{ "key": "temperature", "value": "25.5" },
{ "key": "location", "value": "Building A, Room 101" },
{ "key": "isActive", "value": "true" },
{ "key": "lastMaintenance", "value": "2025-11-01" }
]

Important: All values are stored as strings in the database. Type conversion happens at the UI level.

API Endpoints

Product Metadata

Update Product (including metadata)

PUT /product/:id
Authorization: Bearer <token>
Content-Type: application/json

{
"useDigitalTwin": true,
"metadata": {
"fields": [
{ "key": "temperature", "type": "number" },
{ "key": "location", "type": "string" }
]
}
}

Response: 200 OK with updated ProductDTO

Access: Requires superadmin role

Device Metadata

Get Device (includes metadata)

GET /device/:id
Authorization: Bearer <token>

Response: 200 OK with DeviceDTO including metadata array

Note: Device metadata is included in the main device object. No separate endpoint is needed.

Update Device Metadata

PUT /device/:id/metadata
Authorization: Bearer <token>
Content-Type: application/json

[
{ "key": "temperature", "value": "25.5" },
{ "key": "location", "value": "Building A" }
]

Response: 200 OK with updated device metadata array

Behavior:

  • Validates that all keys exist in the product's metadata structure
  • Automatically removes obsolete fields (keys not in product template)
  • Creates new DeviceMetadata records for new keys
  • Updates existing records for matching keys

Access: Requires authenticated user with device access

Frontend Components

ProductMetadataForm

Location: apps/backoffice-frontend/src/app/[lang]/components/product/form-metadata.tsx

Purpose: Define the metadata structure for a product

Features:

  • Add/remove metadata fields
  • Set field key and type (string, number, boolean, date)
  • Real-time validation
  • Grid layout for clean UI

Usage:

import { ProductMetadataForm } from "@/app/[lang]/components/product/form-metadata";

<ProductMetadataForm product={product} />

Access: Only visible to superadmins (wrapped in AuthorizedBlock)

DeviceMetadataSheet

Location: apps/backoffice-frontend/src/app/[lang]/components/device/device-metadata-sheet.tsx

Purpose: Wrapper component that displays device metadata form in a drawer

Features:

  • Opens in a right-side drawer
  • Displays device description in title
  • Closes automatically on successful save

Usage:

import { DeviceMetadataSheet } from "@/app/[lang]/components/device/device-metadata-sheet";

<DeviceMetadataSheet device={device} roles={session?.roles} />

DeviceMetadataForm

Location: apps/backoffice-frontend/src/app/[lang]/components/device/form/form-update-device-metadata.tsx

Purpose: Edit metadata values for a device based on product template

Features:

  • Dynamically generates form fields based on product metadata structure
  • Type-appropriate inputs (text, number, checkbox, date picker)
  • Validation based on field type
  • Success callback for drawer integration
  • Uses initial metadata from device object (no separate API call)

Props:

interface Props {
device: DeviceDTO;
roles: string[];
onSuccess?: () => void;
initialMetadata?: Array<{ key: string; value: string }>;
}

Technical Flow

1. Product Configuration Flow

User (superadmin) → Product Settings Page
→ Add metadata field (key: "temperature", type: "number")
→ Save
→ PUT /product/:id with metadata structure
→ Product.metadata updated in database

2. Device Value Input Flow

User → Device Settings Page
→ Click "Manage Metadata" button
→ Drawer opens with DeviceMetadataForm
→ Form fields generated from Product.metadata.fields
→ User enters values
→ Save
→ PUT /device/:id/metadata with values array
→ DeviceService.updateMetadata() validates and saves
→ Auto-cleanup removes obsolete fields
→ Drawer closes on success

3. Auto-Cleanup Logic

When updating device metadata:

// In DeviceService.updateMetadata()

1. Fetch product metadata structure
2. Extract valid keys: ["temperature", "location", "isActive"]
3. Filter incoming metadata to only include valid keys
4. Delete existing DeviceMetadata records for this device
5. Create new records with filtered data
6. Return updated metadata array

Example:

  • Product template has: ["temperature", "location"]
  • Device previously had: ["temperature", "location", "oldField"]
  • User updates with: ["temperature", "location", "newField"]
  • Result: Device now has ["temperature", "location"] (both "oldField" and invalid "newField" removed)

Development

Prerequisites

  • Node.js 18+
  • Product with defined metadata structure
  • Superadmin role for product configuration
  • Authenticated user for device metadata editing

Local Development

No special setup required. The feature is integrated into:

  • Product settings page: /dashboard/product/[productId]/settings
  • Device settings page: /dashboard/device/[deviceId]/settings

Testing

Unit Tests: Test metadata validation logic in DeviceService

Integration Tests:

  • Create product with metadata structure
  • Create device for that product
  • Update device metadata
  • Verify auto-cleanup removes obsolete fields

E2E Tests:

  • Navigate to product settings as superadmin
  • Add metadata fields
  • Navigate to device settings
  • Open metadata drawer
  • Enter values and save
  • Verify values persist

Configuration

Environment Variables

No specific environment variables required. Uses existing backend configuration.

Database Schema

The feature uses existing Product and Device tables with JSON fields:

model Product {
id String @id
metadata Json? // Stores metadata structure
// ... other fields
}

model Device {
id String @id
productId String
metadata Json? // Stores metadata values
product Product @relation(fields: [productId], references: [id])
// ... other fields
}

Type Definitions

Supported metadata types:

  • string - Text input
  • number - Number input with validation
  • boolean - Checkbox
  • date - Date picker

Type mapping is handled in:

  • Backend: DTO validation
  • Frontend: Form field generation (getInputForType() function)

Supported Types

TypeUI ComponentValidationExample Value
stringText inputRequired, min 1 char"Building A"
numberNumber inputMust be valid number"25.5"
booleanCheckboxtrue/false only"true"
dateDate pickerValid date format"2025-11-06"

Best Practices

  1. Key Naming: Use descriptive, lowercase keys with no spaces (e.g., "lastMaintenance", not "Last Maintenance")

  2. Type Selection: Choose the most specific type for your data (use number for numeric values, not string)

  3. Required Fields: All metadata fields are required. If optional fields are needed, handle empty strings in your application logic

  4. Cleanup Awareness: Removing a field from product metadata will delete that field from ALL devices of that product

  5. Type Consistency: Don't change field types after devices have data. Create a new field instead

Troubleshooting

Issue: "Field not found in product metadata"

Cause: Device metadata contains a key not present in product metadata structure

Solution: Either add the key to product metadata or remove it from device metadata

Issue: 400 Bad Request when saving product metadata

Cause: Missing required useDigitalTwin field in request

Solution: Ensure useDigitalTwin is included in update payload (handled automatically by ProductMetadataForm)

Issue: Metadata values disappear after saving

Cause: The keys were not present in product metadata template (auto-cleanup removed them)

Solution: First add the keys to product metadata structure, then add values to device

Issue: Cannot save boolean value

Cause: Checkbox state not properly converting to string

Solution: Ensure form handles boolean to string conversion (value should be "true" or "false" as string)

Remarks

  • All metadata values are stored as strings in the database, regardless of type
  • Type conversion happens at the UI layer when rendering form inputs
  • Product metadata structure changes affect all devices of that product
  • Superadmin role required for product metadata configuration
  • No migration path for existing metadata when changing structure (fields are deleted)
  • Maximum metadata fields: No hard limit, but consider UI usability (recommended max ~20 fields)