Field Types
Common field types
There are also several built in Field decorators for common use case:
@Fields.string
A field of type string
@Fields.string()
title = '';
@Fields.number
Just like TypeScript, by default any number is a decimal (or float).
@Fields.number()
price = 1.5
@Fields.integer
For cases where you don't want to have decimal values, you can use the @Fields.integer
decorator
@Fields.integer()
quantity = 0;
@Fields.boolean
@Fields.boolean()
completed = false
@Fields.date
@Fields.date()
statusDate = new Date()
@Fields.dateOnly
Just like TypeScript, by default any Date
field includes the time as well. For cases where you only want a date, and don't want to meddle with time and time zone issues, use the @Fields.dateOnly
@Fields.dateOnly()
birthDate?:Date;
@Fields.createdAt
Automatically set on the backend on insert, and can't be set through the API
@Fields.createdAt()
createdAt = new Date()
@Fields.updatedAt
Automatically set on the backend on update, and can't be set through the API
@Fields.updatedAt()
updatedAt = new Date()
JSON Field
You can store JSON data and arrays in fields.
@Fields.json()
tags: string[] = []
Auto Generated Id Field Types
@Fields.uuid
This id value is determined on the backend on insert, and can't be updated through the API.
@Fields.uuid()
id:string
@Fields.cuid
This id value is determined on the backend on insert, and can't be updated through the API. Uses the @paralleldrive/cuid2 package
@Fields.cuid()
id:string
@Fields.autoIncrement
This id value is determined by the underlying database on insert, and can't be updated through the API.
@Fields.autoIncrement()
id:number
MongoDB ObjectId Field
To indicate that a field is of type object id, change it's fieldTypeInDb
to dbid
.
@Fields.string({
dbName: '_id',
valueConverter: {
fieldTypeInDb: 'dbid',
},
})
id: string = ''
Enum Field
Enum fields allow you to define a field that can only hold values from a specific enumeration. The @Fields.enum
decorator is used to specify that a field is an enum type. When using the @Fields.enum
decorator, an automatic validation is added that checks if the value is valid in the specified enum.
@Fields.enum(() => Priority)
priority = Priority.Low;
In this example, the priority
field is defined as an enum type using the @Fields.enum
decorator. The Priority
enum is passed as an argument to the decorator, ensuring that only valid Priority
enum values can be assigned to the priority
field. The Validators.enum
validation is used and ensures that any value assigned to this field must be a member of the Priority
enum, providing type safety and preventing invalid values.
Literal Fields (Union of string values)
Literal fields let you restrict a field to a specific set of string values using the @Fields.literal
decorator. This is useful for fields with a finite set of possible values.
@Fields.literal(() => ['open', 'closed', 'frozen', 'in progress'] as const)
status: 'open' | 'closed' | 'frozen' | 'in progress' = 'open';
In this example, we use the as const
assertion to ensure that the array ['open', 'closed', 'frozen', 'in progress']
is treated as a readonly array, which allows TypeScript to infer the literal types 'open', 'closed', 'frozen', and 'in progress' for the elements of the array. This is important for the type safety of the status
field.
The status
field is typed as 'open' | 'closed' | 'frozen' | 'in progress'
, which means it can only hold one of these string literals. The @Fields.literal
decorator is used to specify that the status
field can hold values from this set of strings, and it uses the Validators.in
validator to ensure that the value of status
matches one of the allowed values.
For better reusability and maintainability, and to follow the DRY (Don't Repeat Yourself) principle, it is recommended to refactor the literal type and the array of allowed values into separate declarations:
const statuses = ['open', 'closed', 'frozen', 'in progress'] as const;
type StatusType = typeof statuses[number];
@Fields.literal(() => statuses)
status: StatusType = 'open';
In this refactored example, statuses
is a readonly array of the allowed values, and StatusType
is a type derived from the elements of statuses
. The @Fields.literal
decorator is then used with the statuses
array, and the status
field is typed as StatusType
. This approach makes it easier to manage and update the allowed values for the status
field, reducing duplication and making the code more robust and easier to maintain.
ValueListFieldType
Overview
The ValueListFieldType
is useful in cases where simple enums and unions are not enough, such as when you want to have more properties for each value. For example, consider representing countries where you want to have a country code, description, currency, and international phone prefix.
Defining a ValueListFieldType
Using enums or union types for this purpose can be challenging. Instead, you can use the ValueListFieldType
:
@ValueListFieldType()
export class Country {
static us = new Country('us', 'United States', 'USD', '1')
static canada = new Country('ca', 'Canada', 'CAD', '1')
static france = new Country('fr', 'France', 'EUR', '33')
constructor(
public id: string,
public caption: string,
public currency: string,
public phonePrefix: string,
) {}
}
Using in an Entity
In your entity, you can define the field as follows:
@Field(() => Country)
country: Country = Country.us;
Accessing Properties
The property called id
will be stored in the database and used through the API, while in the code itself, you can use each property:
call('+' + person.country.phonePrefix + person.phone)
Note: Only the id
property is saved in the database and used in the API. Other properties, such as caption
, currency
, and phonePrefix
, are only accessible in the code and are not persisted in the database.
Getting Optional Values
To get the optional values for Country
, you can use the getValueList
function, which is useful for populating combo boxes:
console.table(getValueList(Country))
Special Properties: id and caption
The id
and caption
properties are special in that the id
will be used to save and load from the database, and the caption
will be used as the display value.
Automatic Generation of id and caption
If id
and/or caption
are not provided, they are automatically generated based on the static member name. For example:
@ValueListFieldType()
export class TaskStatus {
static open = new TaskStatus() // { id: 'open', caption: 'Open' }
static closed = new TaskStatus() // { id: 'closed', caption: 'Closed' }
id!: string
caption!: string
constructor() {}
}
In this case, the open
member will have an id
of 'open'
and a caption
of 'Open'
, and similarly for the closed
member.
Handling Partial Lists of Values
In cases where you only want to generate members for a subset of values, you can use the getValues
option of @ValueListFieldType
to specify which values should be included:
@ValueListFieldType({
getValues: () => [
Country.us,
Country.canada,
Country.france,
{ id: 'uk', caption: 'United Kingdom', currency: 'GBP', phonePrefix: '44' }
]
})
This approach is useful when you want to limit the options available for a field to a specific subset of values, without needing to define all possible values as static members.
Warning: TypeScript may throw an error similar to Uncaught TypeError: Currency_1 is not a constructor
.
This happens in TypeScript versions <5.1.6 and target es2022. It's a TypeScript bug. To fix it, upgrade to version >=5.1.6 or change the target from es2022. Alternatively, you can call the ValueListFieldType
decorator as a function after the type:
export class TaskStatus {
static open = new TaskStatus()
static closed = new TaskStatus()
id!: string
caption!: string
constructor() {}
}
ValueListFieldType()(TaskStatus)
Summary
The ValueListFieldType
enables the creation of more complex value lists that provide greater flexibility and functionality for your application's needs beyond what enums and unions can offer. By allowing for additional properties and partial lists of values, it offers a versatile solution for representing and managing data with multiple attributes.
Control Field Type in Database
In some cases, you may want to explicitly specify the type of a field in the database. This can be useful when you need to ensure a specific data type or precision for your field. To control the field type in the database, you can use the fieldTypeInDb
option within the valueConverter
property of a field decorator.
For example, if you want to ensure that a numeric field is stored as a decimal with specific precision in the database, you can specify the fieldTypeInDb
as follows:
@Fields.number({
valueConverter: {
fieldTypeInDb: 'decimal(16,8)'
}
})
price=0;
In this example, the price
field will be stored as a decimal
with 16 digits in total and 8 digits after the decimal point in the database. This allows you to control the storage format and precision of numeric fields in your database schema.
Creating Custom Field Types
Sometimes, you may need to create custom field types to handle specific requirements or use cases in your application. By creating custom field types, you can encapsulate the logic for generating, validating, and converting field values.
Example: Creating a Custom ID Field Type with NanoID
NanoID is a tiny, secure, URL-friendly, unique string ID generator. You can create a custom field type using NanoID to generate unique IDs for your entities. Here's an example of how to create a custom NanoID field type:
import { nanoid } from 'nanoid'
import { Fields, type FieldOptions } from 'remult'
export function NanoIdField<entityType = any>(
...options: FieldOptions<entityType, string>[]
) {
return Fields.string<entityType>(
{
allowApiUpdate: false, // Disallow updating the ID through the API
defaultValue: () => nanoid(), // Generate a new NanoID as the default value
saving: (_, record) => {
if (!record.value) {
record.value = nanoid() // Generate a new NanoID if the value is not set
}
},
},
...options,
)
}
In this example, the NanoIdField
function creates a custom field type based on the Fields.string
type. It uses the nanoid
function to generate a unique ID as the default value and ensures that the ID is generated before saving the record if it hasn't been set yet. This custom field type can be used in your entities to automatically generate and assign unique IDs using NanoID.
Customize DB Value Conversions
Sometimes you want to control how data is saved to the db, or the dto object. You can do that using the valueConverter
option.
For example, the following code will save the tags
as a comma separated string in the db.
@Fields.object<Task, string[]>({
valueConverter: {
toDb: x => (x ? x.join(",") : undefined),
fromDb: x => (x ? x.split(",") : undefined)
}
})
tags: string[] = []
You can also refactor it to create your own FieldType
import { Field, FieldOptions, Remult } from 'remult'
export function CommaSeparatedStringArrayField<entityType = any>(
...options: (
| FieldOptions<entityType, string[]>
| ((options: FieldOptions<entityType, string[]>, remult: Remult) => void)
)[]
) {
return Fields.object(
{
valueConverter: {
toDb: (x) => (x ? x.join(',') : undefined),
fromDb: (x) => (x ? x.split(',') : undefined),
},
},
...options,
)
}
And then use it:
@CommaSeparatedStringArrayField()
tags: string[] = []
There are several ready made valueConverters included in the remult
package, which can be found in remult/valueConverters
Class Fields
Sometimes you may want a field type to be a class, you can do that, you just need to provide an implementation for its transition from and to JSON.
For example:
export class Phone {
constructor(public phone: string) {}
call() {
window.open('tel:' + this.phone)
}
}
@Entity('contacts')
export class Contact {
//...
@Field<Contact, Phone>(() => Phone, {
valueConverter: {
fromJson: (x) => (x ? new Phone(x) : undefined!),
toJson: (x) => (x ? x.phone : undefined!),
},
})
phone?: Phone
}
Alternatively you can decorate the Phone
class with the FieldType
decorator, so that whenever you use it, its valueConverter
will be used.
@FieldType<Phone>({
valueConverter: {
fromJson: (x) => (x ? new Phone(x) : undefined!),
toJson: (x) => (x ? x.phone : undefined!),
},
})
export class Phone {
constructor(public phone: string) {}
call() {
window.open('tel:' + this.phone)
}
}
@Entity('contacts')
export class Contact {
//...
@Field(() => Phone)
phone?: Phone
}