On this page

Fields

Fields are models that represent database columns or the values of their nested objects. They can be defined within blocks or collections.


Standard fields

There are 27 default field types provided by Pruvious. Here is an overview of each type:

FieldDescription

Used to display a small set of options that users can easily switch between.

A fundamental field that stores a boolean value.

Stores an array of strings that represent multiple choices.

An alternative to checkboxes. It stores an array of strings and can efficiently display a large number of choices in the user interface.

Stores a timestamp in milliseconds to represent a date value.

Stores a tuple of timestamps in milliseconds that represent two date values.

Stores a timestamp in milliseconds to represent a date-time value.

Stores a tuple of timestamps in milliseconds that represent two date-time values.

Rich text editor that stores text content in HTML format.

Stores a relationship to a record in the uploads collection.

Used to display an icon component from the icons directory in the project's root.

Stores a relationship to an image record in the uploads collection.

Stores links to page-like collections or external links.

A fundamental field that stores a numeric value.

Stores a tuple of numbers.

Stores a relationship to a record in a specific collection.

Stores relationships to multiple records from a specified collection as an array of integers.

Stores an array of subfield values.

Displays a dropdown list of numerous choices.

Stores an object that includes numeric values along with their corresponding units.

Stores a numeric value and can be used as a substitute for the number field.

Stores a tuple of numbers and can be used instead of the range field.

Stores a boolean value and can be used as an alternative to the checkbox field.

The most frequently used field for storing strings.

Stores strings with line breaks.

Stores a timestamp in milliseconds to represent a time value.

Stores a tuple of timestamps in milliseconds that represent two time values.

Defining fields

Fields can be defined in blocks or collections.

In blocks

You can define fields directly in the Vue component by using field macros in defineProps. To import field macros, use the #pruvious alias. The macro names follow the convention of starting with the camelCased field type and ending with Field or Subfield (e.g., textField, imageField, dateTimeSubfield, etc.).

Here is an example of using the editorField in a prose block:

// blocks/Prose.vue

<template>
  <PruviousHTML :html="content" class="prose" />
</template>

<script lang="ts" setup>
import { editorField } from '#pruvious'

defineProps({
  content: editorField({
    required: true,
    toolbar: ['heading1', 'heading2', 'paragraph', 'bold', 'italic'],
    blockFormats: [{ className: 'whitespace-nowrap', label: 'No wrap' }],
    inlineFormats: [{ className: 'badge', label: 'Badge' }],
  }),
})
</script>

Each field macro is fully typed, and its options are well documented in the code.

In collections

Fields in collections are defined as simple objects. Below is an example of using the text and number fields in a product collection:

# collections/products.ts

import { defineCollection } from '#pruvious'

export default defineCollection({
  name: 'products',
  mode: 'multi',
  fields: {
    name: {
      type: 'text',
      options: {
        required: true,
      },
    },
    price: {
      type: 'number',
      options: {
        required: true,
        decimals: 2,
        min: 0,
      },
    },
  },
})

Similar to block macros, the code is well-documented and you can find more information and examples by inspecting the defineCollection arguments in your IDE.

Conditional logic

Conditional logic allows fields to depend on the values of other fields. For example, you can show or hide a field based on the value of a sibling field or even fields beyond the current scope.

Here is an example of conditional image and video fields that depend on the value of a button-group field:

# collections/visuals.ts

import { defineCollection } from '#pruvious'

export default defineCollection({
  name: 'visuals',
  mode: 'multi',
  fields: {
    type: {
      type: 'button-group',
      options: {
        choices: { image: 'Image', video: 'Video' },
        default: 'image',
      },
    },
    image: {
      type: 'image',
      options: {
        required: true,
      },
      additional: {
        conditionalLogic: {
          type: 'image',
        }
      }
    },
    video: {
      type: 'file',
      options: {
        required: true,
        allowedTypes: ['video/mp4', 'video/webm', 'video/ogg'],
      },
      additional: {
        conditionalLogic: {
          type: 'video',
        }
      }
    },
  },
})

Fields that do not comply with this logic are set to their default values, and further validation is skipped.

You can use relative paths to reference fields in the conditionalLogic property (e.g., ../parentField, /topLevelField, etc.). Dot notation allows access to child fields, such as parentField.childField. You can also target fields in a specific array element, like repeaterField.0.subField.

The conditionalLogic object can be defined in multiple ways:

# examples/conditional-logic.ts

import type { ConditionalLogic, ConditionalRule } from '#pruvious'

let cl: ConditionalLogic | Record<string, ConditionalRule | boolean | number | string>

// Match if sibling field 'foo' is true
cl = { foo: true }

// Match if parent (repeater) has more than 2 items
cl = { '..': { gt: 2 } }

// Match if 'foo' is true and 'bar' is 'baz' or 'qux'
cl = { foo: true, $some: [{ bar: 'baz' }, { bar: 'qux' }] }
// More verbose alternative:
cl = { $every: [{ foo: true }, { $some: [{ bar: 'baz' }, { bar: 'qux' }] }] }

Here are the definitions of the ConditionalLogic and ConditionalRule types for your reference:

# #pruvious

type ConditionalLogic =
  | { $every: (Record<string, ConditionalRule | boolean | number | string> | ConditionalLogic)[] }
  | { $some: (Record<string, ConditionalRule | boolean | number | string> | ConditionalLogic)[] }

type ConditionalRule =
  | { eq: boolean | number | string }
  | { ne: boolean | number | string }
  | { gt: number | string }
  | { gte: number | string }
  | { lt: number | string }
  | { lte: number | string }
  | { regexp: string }

You can also use conditional logic in field macros within blocks. The additional property shown in the example for collections is the second argument of the field macro function.

# blocks/Visual.vue

<template>
  <div>...</div>
</template>

<script lang="ts" setup>
import { buttonGroupField, fileField, imageField } from '#pruvious'

defineProps({
  type: buttonGroupField({
    choices: { image: 'Image', video: 'Video' },
    default: 'image',
  }),
  image: imageField(
    { required: true },
    { conditionalLogic: { type: 'image' } }
  ),
  video: fileField(
    { required: true, allowedTypes: ['video/mp4', 'video/webm', 'video/ogg'] },
    { conditionalLogic: { type: 'video' } },
  ),
})
</script>

Sanitizers

Sanitizers are used to clean and prepare field values before storing them in the database. They are executed sequentially in the specified order and can be asynchronous.

Here is an example of a collection with a text field that sanitizes the input value by converting the string to lowercase:

# collections/employees.ts

import { defineCollection } from '#pruvious'

export default defineCollection({
  name: 'employees',
  mode: 'multi',
  fields: {
    email: {
      type: 'text',
      options: { trim: true },
      additional: {
        sanitizers: [
          (context) => {
            return typeof context.value === 'string'
              ? context.value.toLowerCase()
              : context.value
          },
        ],
      },
    },
  },
})

Field-specific sanitizers, such as trim from the example above, are executed prior to the ones specified in the additional property. It is important to note that the input value can be anything, so you should check its type before processing it. The sanitizer function must always return a value, even if it has not modified the input value.

The sanitizer function is provided with a context argument, which is an object containing the following properties:

PropertyDescription

definition

The resolved field definition object.

fields

An object representing all field definitions using key-value pairs.

input

A key-value object containing raw input values.

name

The property name used for declaring the field in a collection or block.

operation

The current query operation.

options

The resolved field options used when declaring the field in a collection or block.

query

Utility function for creating a new query builder.

value

The field value.

Sanitizers can be executed on specific query operations. You can check the current operation using the operation property in the context argument or define your sanitizers as an object.

Here's an example of a block with an asynchronous text sanitizer that runs only when creating a field value:

# blocks/Employee.vue

<template>
  <div>...</div>
</template>

<script lang="ts" setup>
import { textField } from '#pruvious'

defineProps({
  text: textField(
    { trim: true },
    { sanitizers: [{
        onCreate: true,
        sanitizer: async ({ value, query }) => {
          if (typeof value === 'string') {
            const employee = await query('employees')
              .select({ email: true })
              .where('altEmail', value.toLowerCase())
              .first()

            if (employee) {
              return employee.email
            }
          }

          return value
        },
      }],
    },
  ),
})
</script>

As seen in the example above, sanitizers can be applied in the second argument of field macros within blocks.

Most standard fields already have implemented sanitizers that cover common use cases. For instance, if you provide a string to the number field, it will be cast to a number before applying other sanitizers and validators.

Validators

Validators are functions that validate field values before storing and retrieving them from the database. They enforce specified conditions and generate errors if the conditions are not met. Validators are executed in the order they are provided, can be asynchronous, and run after sanitizers.

For instance, consider a text field in a collection that verifies if the user has entered a valid ZIP code.

# collections/addresses.ts

import { defineCollection } from '#pruvious'

export default defineCollection({
  name: 'addresses',
  mode: 'multi',
  fields: {
    zip: {
      type: 'text',
      options: { required:true },
      additional: {
        validators: [
          (context) => {
            if (!/(^\d{5}$)|(^\d{5}-\d{4}$)/.test(context.value)) {
              throw new Error('Invalid zip code')
            }
          },
        ],
      },
    },
  },
})

Field specific validators, such as required from the example above, are executed before the validators specified in the additional property.

The validator function receives a context argument, which is an object that includes the following properties:

PropertyDescription

_

A helper function used to display validation errors in the default translation domain and a specific language.

__

A helper function used to display validation errors in a specific translation domain and language.

allInputs

All sanitized and casted field inputs or records (only available when creating or reading multiple records at once).

collection

The resolved definition of the currently queried collection.

collections

An object containing all collection definitions.

currentQuery

The current query builder instance.

definition

The resolved field definition object.

errors

A key-value object with the current validation errors.

fields

An object representing all field definitions using key-value pairs.

input

An object containing sanitized and casted fields for processing.

language

Preferred user language for displaying validation errors.

name

The property name used for declaring the field in a collection or block.

operation

The current query operation.

options

The resolved field options used when declaring the field in a collection or block.

query

Utility function for creating a new query builder.

value

The field value.

Validators can be executed on specific query operations. To determine the current operation, you can use the operation property in the context argument. Alternatively, you can define your validators as an object.

Here's an example of a block that demonstrates an asynchronous text validator. This validator only runs when creating or updating a field value.

# blocks/Address.vue

<template>
  <div>...</div>
</template>

<script lang="ts" setup>
import { textField } from '#pruvious'

defineProps({
  zip: textField(
    { required: true },
    { validators: [{
        onCreate: true,
        onUpdate: true,
        validator: async ({ value, _, language }) => {
          const validateZipCode = await import('../utils/address.ts')

          if (!(await validateZipCode(value))) {
            return _(language, 'Invalid zip code')
          }
        },
      }],
    },
  ),
})
</script>

Validators can be applied in the second argument of field macros within blocks, as shown in the example above.

Population

In the database, certain fields may have different values stored compared to when they are read. For instance, record and image fields store the ID of the related collection record in the database, but when retrieved, they are filled with the data from the corresponding record. When a page is fetched for rendering, all field values are in their populated state. To specify the population state, the populate parameter must be set to true when using the query builder or collection API.

You have the option to customize the field populators for each defined field individually. Here's an example of a number field that changes its type and value upon population:

# collections/credit-cards.ts

import { defineCollection } from '#pruvious'

export default defineCollection({
  name: 'credit-cards',
  mode: 'multi',
  fields: {
    number: {
      type: 'number',
      options: { min: 16, max: 16 },
      additional: {
        population: {
          type: 'string',
          populator: (context) => {
            // Output: "**** **** **** XXXX"
            return context.value
              .toString()
              .replace(/\d(?=\d{4})/g, '*')
              .replace(/(.{4})/g, '$1 ')
          },
        },
      },
    },
  },
})

The population object should include the properties type and populator. The type property may differ from the stored field type. The populator function can be asynchronous and accepts a context argument with the following properties:

PropertyDescription

currentQuery

The current query builder instance.

definition

The resolved field definition object.

fields

An object representing all field definitions using key-value pairs.

name

The property name used for declaring the field in a collection or block.

options

The resolved field options used when declaring the field in a collection or block.

query

Utility function for creating a new query builder.

value

The field value.

Populators are executed after sanitizers and validators, but only when reading from the database. They can also be defined for block fields by providing a second argument to the field macro, similar to sanitizers and validators.

Here's an example of the same field shown in the previous collection example, but in a block:

# blocks/CreditCard.vue

<template>
  <div>...</div>
</template>

<script lang="ts" setup>
import { numberField } from '#pruvious'

defineProps({
  number: numberField(
    { min: 16, max: 16 },
    {
      population: {
        type: 'string',
        populator: ({ value }) => {
          // Output: "**** **** **** XXXX"
          return value
            .toString()
            .replace(/\d(?=\d{4})/g, '*')
            .replace(/(.{4})/g, '$1 ')
        },
      },
    },
  ),
})
</script>

Guards

Guards are security layers that prevent unauthorized access to specific fields. Here's an example of a field guard:

# collections/api-keys.ts

import { defineCollection, hasCapability } from '#pruvious'

export default defineCollection({
  name: 'api-keys',
  label: 'API Keys',
  mode: 'single',
  fields: {
    cloudflare: {
      type: 'text',
      options: {},
      additional: {
        guards: [
          async ({ user }) => {
            if (!hasCapability(user, 'manage-cloudflare-api-key')) {
              throw new Error('You are not allowed to manage the Cloudflare API key')
            }
          },
        ],
      },
    },
  },
})

The guards are executed in the given order and can be assigned for specific CRUD operations. Pruvious also provides collection guards that can be added in the collection definition.

Last updated on January 14, 2024 at 13:24