npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wisemen/nestjs-custom-fields

v6.0.0

Published

Custom field definitions and values for NestJS applications with TypeORM persistence, DTO support, and runtime validation.

Readme

@wisemen/nestjs-custom-fields

Custom field definitions and values for NestJS applications with TypeORM persistence, DTO support, and runtime validation.

Custom field definitions are developer-managed. This package does not target end-user managed custom field creation.

It is intended that custom field definitions are read-only for users. Creating, updating, and deleting definitions is handled manually by developers through migrations or playground.ts scripts.

Definition uniqueness is scoped by entityType. For non-tenant definitions, key must be unique per entityType. For tenant-scoped definitions, key + tenantUuid must be unique per entityType.

Step 1: Register The Exported Entity And Create The Table

This package already exports the effective CustomFieldDefinition TypeORM entity. Add it to all TypeORM datasources yourself so TypeORM knows about the table metadata. After that, generate and run a migration so the table and the exported entity's partial unique indexes are created in your database.

import { DataSource } from 'typeorm'
import { CustomFieldDefinition } from '@wisemen/nestjs-custom-fields'

const datasource = new DataSource({
  entities: ['dist/src/**/*.entity.js', CustomFieldDefinition]
})

Step 2: Define Definitions And Insert Them In The Database

Create a definition with customFieldDefinition(...), then insert it yourself. The package does not discover definitions automatically. At the moment this is mainly done through a migration or through playground.ts.

For persisted definitions, tenantUuid is part of the definition shape and should be null for non-tenant definitions or a tenant UUID for tenant-scoped definitions. This is different from repository lookups, where tenantUuid is optional and can simply be omitted.

key uniqueness is enforced per entityType. Use a unique key for global definitions, and a unique key + tenantUuid combination for tenant-specific definitions.

It is recommended to define your entityType values in an enum instead of repeating string literals across your application.

import type { INestApplicationContext } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { JobContainer } from '@wisemen/app-container/fastify'
import { LocalizedString } from '@wisemen/localized-string'
import { DataSource } from 'typeorm'
import {
  CustomFieldDefinition,
  CustomFieldType,
  customFieldDefinition
} from '@wisemen/nestjs-custom-fields'

enum CustomFieldEntityType {
  TICKET = 'ticket'
}

export class Playground extends JobContainer {
  async bootstrap(): Promise<INestApplicationContext> {
    return await NestFactory.createApplicationContext(PlayGroundModule)
  }

  async execute(app: INestApplicationContext): Promise<void> {
    const priorityField = customFieldDefinition(CustomFieldType.SINGLE_SELECT, {
      tenantUuid: null,
      entityType: CustomFieldEntityType.TICKET,
      key: 'priority',
      label: new LocalizedString([{ locale: 'en', value: 'Priority' }]),
      description: null,
      isRequired: true,
      choices: [
        {
          value: 'low',
          order: 1,
          label: new LocalizedString([{ locale: 'en', value: 'Low' }])
        },
        {
          value: 'high',
          order: 2,
          label: new LocalizedString([{ locale: 'en', value: 'High' }])
        }
      ]
    })

    await app.get(DataSource).manager.insert(CustomFieldDefinition, priorityField)
  }
}

Views

Use views to control where a definition is shown. A definition is shown in a view only when that view's key is in its views array; any view not listed does not show it. order is the field's position within that view, so the same field can be first in a detail page and last in a table. The package does not sort by order; sort by it where you render the view.

View keys are app-defined strings, like entityType: the package stores them as-is. Keep them in an enum shared with your frontend. views defaults to [], so a definition without views is hidden everywhere. View keys must be non-empty and unique per definition, and order must be a finite number.

const referenceField = customFieldDefinition(CustomFieldType.TEXT, {
  tenantUuid: null,
  entityType: CustomFieldEntityType.TICKET,
  key: 'reference',
  label: new LocalizedString([{ locale: 'en', value: 'Reference' }]),
  description: null,
  isRequired: false,
  views: [
    { key: 'ticket-detail', order: 10 },
    { key: 'ticket-overview', order: 20 }
  ]
})

When upgrading from 1.x to 2.0.0, generate a migration for the new views column and backfill views for your existing definitions in the same release. Without a backfill, every existing definition is hidden in every view.

Choice Settings

Choices of single and multi select fields take three optional settings:

  • isArchived: the choice can't be selected anew. Records that already have it keep it and can be saved unchanged. Pass the stored values as previousValues to validateCustomFieldValues(...) so it can tell the two apart. Show archived choices on records and in filters, but leave them out of the options of a form.
  • group: a stable key to group choices, e.g. for reporting. The package only stores it.
  • dependsOn: { definitionKey, values }. The choice can only be selected when the definition with that key has one of those values. A choice without dependsOn can always be selected.
const componentField = customFieldDefinition(CustomFieldType.MULTI_SELECT, {
  tenantUuid: null,
  entityType: CustomFieldEntityType.TICKET,
  key: 'component',
  label: new LocalizedString([{ locale: 'en', value: 'Component' }]),
  description: null,
  isRequired: false,
  choices: [
    {
      value: 'login_page',
      label: new LocalizedString([{ locale: 'en', value: 'Login page' }]),
      order: 1,
      group: 'frontend',
      dependsOn: { definitionKey: 'category', values: ['web'] }
    },
    {
      value: 'legacy_portal',
      label: new LocalizedString([{ locale: 'en', value: 'Legacy portal' }]),
      order: 2,
      isArchived: true
    }
  ]
})

The parent definition must be part of the definitions you validate against, and its value part of the values: validate the full set of values of a record, not only the ones that changed. A group can't be empty, a choice can't depend on its own definition, and dependsOn.values can't be empty.

Reference Fields

Use CustomFieldType.REFERENCE for a field that points to another record. The value is the referenced record's ID as a non-empty string. The package does not know which entity is referenced: map the definition's entityType and key to the right route in your frontend.

Rules are optional. format restricts the ID to a UUID (ReferenceCustomFieldFormat.UUID), digits only (ReferenceCustomFieldFormat.NUMERIC) or letters and digits only (ReferenceCustomFieldFormat.ALPHANUMERIC), and regex adds a custom pattern. When both are set, the value must pass both.

import { ReferenceCustomFieldFormat } from '@wisemen/nestjs-custom-fields'

const customerField = customFieldDefinition(CustomFieldType.REFERENCE, {
  tenantUuid: null,
  entityType: CustomFieldEntityType.TICKET,
  key: 'customer',
  label: new LocalizedString([{ locale: 'en', value: 'Customer' }]),
  description: null,
  isRequired: false,
  rules: {
    format: ReferenceCustomFieldFormat.UUID
  }
})

When upgrading from 3.x to 4.0.0, handle CustomFieldType.REFERENCE wherever you switch over CustomFieldType or build a Record<CustomFieldType, ...>; those no longer type-check until the new case is added. No database migration is needed: type is stored as varchar and rules as jsonb.

Progress Fields

Use CustomFieldType.PROGRESS for a progress bar. The definition's choices are the bar's segments, and rules set a ProgressCustomFieldColor for every choice and an optional icon. The value is { total?, segments }: segments is a list of { key, amount }, where key is a choice value and amount is a finite number of at least 0. Keys can appear only once. A missing segment counts as 0, so { segments: [] } is a valid value.

Each segment fills amount / total of the bar. Set total when it is known, such as 100 for a percentage: it must be greater than 0, the amounts can not add up to more than it, and the rest of the bar stays empty. Leave total out when it is not known: the total is then the sum of the amounts and the bar is always fully filled.

import { ProgressCustomFieldColor } from '@wisemen/nestjs-custom-fields'

const tasksField = customFieldDefinition(CustomFieldType.PROGRESS, {
  tenantUuid: null,
  entityType: CustomFieldEntityType.TICKET,
  key: 'tasks',
  label: new LocalizedString([{ locale: 'en', value: 'Tasks' }]),
  description: null,
  isRequired: false,
  choices: [
    {
      value: 'completed',
      order: 1,
      label: new LocalizedString([{ locale: 'en', value: 'Completed' }])
    },
    {
      value: 'in_progress',
      order: 2,
      label: new LocalizedString([{ locale: 'en', value: 'In progress' }])
    }
  ],
  rules: {
    icon: 'tasks',
    colors: {
      completed: ProgressCustomFieldColor.SUCCESS,
      in_progress: ProgressCustomFieldColor.WARNING
    }
  }
})

// 2 completed and 3 in progress, total unknown: 2/5 green, 3/5 yellow
const value = {
  segments: [
    { key: 'completed', amount: 2 },
    { key: 'in_progress', amount: 3 }
  ]
}

// 20% completed: 20/100 green, the rest empty
const percentage = {
  total: 100,
  segments: [{ key: 'completed', amount: 20 }]
}

The icon is an app-defined string, like entityType: the package stores it as-is. Leave it out to render the bar without an icon; an empty string is rejected.

When upgrading from 4.x to 5.0.0, handle CustomFieldType.PROGRESS wherever you switch over CustomFieldType or build a Record<CustomFieldType, ...>; those no longer type-check until the new case is added. No database migration is needed: type is stored as varchar, and choices and rules as jsonb.

Step 3: Store Values On The Entity With The Custom Column

Resolved custom field values live on your own entity. Use @CustomFieldValueColumn() for the persisted values column.

import { Entity, PrimaryGeneratedColumn } from 'typeorm'
import {
  CustomFieldValueColumn,
  type CustomFieldValue
} from '@wisemen/nestjs-custom-fields'

@Entity()
export class Ticket {
  @PrimaryGeneratedColumn('uuid')
  uuid: string

  @CustomFieldValueColumn({ nullable: true })
  customFields: CustomFieldValue[] | null
}

Step 4: Receive And Show Values With DTOs

Requests and responses use the DTO layer from this package. For incoming payloads, use CustomFieldValueDto with @IsCustomFields(). For outgoing payloads, map stored values back with CustomFieldValueDto.from(...).

Calling parse() on a DTO turns the transport-friendly input into the domain custom field value shape, including richer value objects when needed. For example, a MonetaryDto is parsed into the package's monetary value object instead of staying a plain JSON structure.

import { ApiProperty } from '@nestjs/swagger'
import {
  CustomFieldValueApiExtraModels,
  CustomFieldValueDto,
  CustomFieldValueDtoApiProperty,
  IsCustomFields
} from '@wisemen/nestjs-custom-fields'

@CustomFieldValueApiExtraModels()
export class UpdateTicketCommand {
  @CustomFieldValueDtoApiProperty({ isArray: true })
  @IsCustomFields()
  customFields: CustomFieldValueDto[]
}

@CustomFieldValueApiExtraModels()
export class TicketResponseDto {
  @ApiProperty({ type: String, format: 'uuid' })
  uuid: string

  @CustomFieldValueDtoApiProperty({ isArray: true })
  customFields: CustomFieldValueDto[]

  constructor(ticket: Ticket) {
    this.uuid = ticket.uuid
    this.customFields = ticket.customFields?.map(value => CustomFieldValueDto.from(value)) ?? []
  }
}

@IsCustomFields() already applies array validation, nested DTO validation, and uniqueness on definitionUuid.

import {
  CustomFieldType,
  MonetaryCustomFieldValueDto
} from '@wisemen/nestjs-custom-fields'

const dto = new MonetaryCustomFieldValueDto()
dto.definitionUuid = definitionUuid
dto.value = {
  amount: '12.50',
  currency: 'EUR',
  precision: 2
}

const parsed = dto.parse()

// parsed is now a domain custom field value object:
// {
//   definitionUuid,
//   type: CustomFieldType.MONETARY,
//   value: dto.value.parse()
// }

Step 5: Retrieve Definitions For Validation

When you process submitted values, retrieve the matching definitions through CustomFieldDefinitionsRepository and validate against them with validateCustomFieldValues(...). If you also want to return definitions from an API, map the same repository results with CustomFieldDefinitionResponse. To inject the repository, register CustomFieldDefinitionRepositoryModule in the Nest module that owns your use case or controller.

validateCustomFieldValues(...) throws CustomFieldValueValidationError when the submitted values do not match the definitions. When a record is updated, pass its stored values as the third argument, validateCustomFieldValues(definitions, values, { previousValues }), so archived choices it already has stay valid (see Choice Settings). In controllers, document that error according to the @wisemen/api-error package pattern by registering it with @ApiErrorResponse(...).

import { Module } from '@nestjs/common'
import { CustomFieldDefinitionRepositoryModule } from '@wisemen/nestjs-custom-fields'

@Module({
  imports: [CustomFieldDefinitionRepositoryModule],
  providers: [UpdateTicketCustomFieldsUseCase]
})
export class UpdateTicketModule {}

On repository lookups, tenantUuid is optional. Omit it to work with non-tenant definitions, or set it to retrieve tenant-specific definitions in a multi-tenant setup.

Pass view to retrieve only the definitions shown in that view. Results are ordered by key, with or without view.

const definitions = await repository.findDefinitions({
  entityType: CustomFieldEntityType.TICKET,
  view: 'ticket-overview'
})

After DTO parsing, validate the resolved values against the definitions that apply for that entity and tenant. This is the runtime check that enforces the definition rules.

import { Injectable } from '@nestjs/common'
import {
  CustomFieldDefinitionResponse,
  CustomFieldDefinitionsRepository,
  validateCustomFieldValues
} from '@wisemen/nestjs-custom-fields'

enum CustomFieldEntityType {
  TICKET = 'ticket'
}

@Injectable()
export class UpdateTicketCustomFieldsUseCase {
  constructor(
    private readonly repository: CustomFieldDefinitionsRepository
  ) {}

  async execute(command: UpdateTicketCommand): Promise<void> {
    const definitions = await this.repository.findDefinitions({
      entityType: CustomFieldEntityType.TICKET
    })

    const values = command.customFields.map(value => value.parse())

    validateCustomFieldValues(definitions, values)

    // Persist the validated values here.
  }
}

If you want to return definitions from a read endpoint, use CustomFieldDefinitionResponse to map the repository results into the response shape:

async function findTicketDefinitions(
  repository: CustomFieldDefinitionsRepository
): Promise<CustomFieldDefinitionResponse[]> {
  const definitions = await repository.findDefinitions({
    entityType: CustomFieldEntityType.TICKET
  })

  return definitions.map(definition => new CustomFieldDefinitionResponse(definition))
}
import { ApiErrorResponse } from '@wisemen/api-error'
import { CustomFieldValueValidationError } from '@wisemen/nestjs-custom-fields'

@ApiErrorResponse(CustomFieldValueValidationError)
async update(...): Promise<void> {
  // controller logic
}