Skip to content

PDM Language ​

A Portable Data Module (PDM) is a text file that defines one integration. The file extension is .pdm. One file holds one module.

A module can hold these parts:

  • A header that names the module
  • Configuration values, for example API keys
  • Models that define the shape of the data
  • Actions that read or write data in an external system
  • Mappings that convert data from one model to another model

The IDE extension gives syntax highlighting, completion and validation for .pdm files.

File structure ​

The parts of a module must be in this order:

  1. vendor and name (required)
  2. version (optional)
  3. meta (optional)
  4. defaults (optional)
  5. All other parts, in any order: import, config, model, type, mapping and mapFn

If a part is not in this order, the parser shows an error.

Precept uses line breaks to separate entries. Commas between entries are optional in most blocks.

Comments ​

pdm
vendor acme
name comments

// A comment to the end of the line

/* A comment
   on more than one line */
model Note {
  text: string // A comment after a field
}

The header names the module. vendor and name together identify the module. The values can be plain identifiers or quoted strings.

pdm
vendor acme
name crm
version "1.0"

meta {
  productName: "Acme CRM"
  description: "Contacts from the Acme CRM API"
}
KeywordRequiredDescription
vendorYesThe organization that owns the module
nameYesThe name of the module
versionNoThe version of the module
metaNoKey-value pairs that describe the module, for example productName, description and icon

The full name of a model has the form vendor/name.Model, for example acme/crm.Contact. API requests use this full name.

Imports ​

An import makes the models of another module available. Use the name of the other module, in the form vendor/name. Then refer to its models with the module name as a prefix.

For example, after import official/genericCrm, the model Person in that module is genericCrm.Person.

Configuration ​

A config block declares the values that each installation of the module must supply. Actions read these values as config.<name>.

pdm
vendor acme
name settings

config {
  baseUrl: string
  username: string
  password: secret string
  retries?: number
}
  • Put ? after the name to make a value optional.
  • Put secret before the type to mark the value as a secret.

OAuth 2.0 ​

A configuration value can hold an OAuth 2.0 flow. Actions read the token as config.auth.accessToken.token and the token type as config.auth.accessToken.type. In these paths, auth is the name of the configuration value.

pdm
vendor acme
name oauth

config {
  auth: oauth2.authCodeFlow {
    authorizationUrl: "https://api.example.com/oauth/authorize"
    tokenUrl: "https://api.example.com/oauth/token"
    scope: "login data"
  }
}

The available flows are oauth2.authCodeFlow, oauth2.clientCredentials, oauth2.implicitFlow, oauth2.customFlow and oauth2.jwtBearer. See OAuth 2.0 connections for every setting, client authentication and provider examples.

Defaults ​

A defaults block sets values that all declarative actions in the module use. The http block sets the base URL and the headers of each request.

pdm
vendor acme
name httpdefaults

defaults {
  http {
    baseUrl: "https://api.example.com"
    headers: {
      Authorization: `Bearer {config.apiKey}`
    }
  }
}

config {
  apiKey: secret string
}

A resource block can also have a defaults block. Those values apply only to the actions in that resource.

Models ​

A model defines the shape of one record. Each line in a model is a field.

pdm
vendor acme
name models

model Address {
  street: string
  city: string
  zip: string
}

model Contact {
  id: string
  name: string
  age?: number
  active: boolean
  tags: string[]
  address: Address
  previous: Address[]
  "e-mail": string
}
  • The form of a field is name: type.
  • Put ? after the name to make the field optional.
  • Put a field name in quotes if the name has characters that are not letters, digits or _.

Types ​

TypeExampleDescription
stringname: stringText
numberage: numberA number
booleanactive: booleantrue or false
A modeladdress: AddressA record of another model
An arraytags: string[]A list of values of one type
A literalkind: "person"Exactly this value
A unionstatus: "active" | "inactive"One of the types in the list
A named typestatus: StatusA type that a type statement defines

A type statement gives a name to a type. Use it when more than one field has the same type.

pdm
vendor acme
name types

type Status = "active" | "inactive" | "pending"

model Account {
  status: Status
  previousStatus?: Status
}

Annotations ​

An annotation adds a rule or a description to a field. You can write annotations on the same line as the field. You can also write them on the lines after the field, each line with a - at the start.

pdm
vendor acme
name annotations

model Contact {
  id: string primary
  email: string format("email") maxLength(254)
  name: string
    - title("Full name")
    - description("The first and last name of the contact")
  score: number range(min: 0, max: 100)
}

These are all the annotations that Precept knows:

AnnotationArgumentDescription
primaryNoneThe field identifies the record.
uniqueNoneNo two records have the same value.
readonlyNoneThe field is read-only.
secretNoneThe value is sensitive.
titleTextA short label for the field
descriptionTextA longer description of the field
exampleTextA sample value
defaultTextThe default value of the field
piiTextThe field holds personal data. The argument names the kind of data.
formatTextThe format of a string: "email", "url", "uuid", "datetime", "date" or "ip"
lengthNumberThe exact number of characters. See length.
offsetNumberThe number of characters to skip after the previous field. See offset.
maxLengthNumberThe maximum number of characters
minLengthNumberThe minimum number of characters
precisionNumberThe maximum number of digits after the decimal point
rangemin: number, max: numberThe lowest and highest value of a number
relationIdentifierThe field refers to another model.
fileTextThe settings page offers a file upload for the field. The argument lists the file types, for example ".pem,.crt". Use it on string config fields only. See Client certificates.

If you use another name, the parser shows a warning: Unknown annotation '<name>'.

When response validation is on, Precept applies format, length, maxLength, minLength, precision and range to the data that an action returns.

length ​

length(n) sets the width of a field in characters.

pdm
vendor acme
name holdings

model Holding {
  account: string length(9)
  symbol: string length(6)
  units: string
    - length(12)
}

length has two effects:

  • Fixed-width files. In data ingestion, turn on Fixed-length in the file read rule. Then Precept reads each line of the file by position. The first field is characters 1 to 9. The next field starts after it, and so on. Precept removes spaces from the start and end of each value. Each field of the model must have a length annotation.
  • Validation. When response validation is on, a string value must have exactly n characters.

The inline form length(9) and the line form - length(12) have the same effect.

offset ​

offset(n) moves the start of a field in a fixed-width file. The argument is a whole number, without quotes. offset("2"), offset(-1) and offset(1.5) are errors.

offset is always relative to the end of the previous field. It is never an absolute position. The number is the count of characters to skip between the end of the previous field and the start of this field. On the first field, it moves the start of the line. A field without offset starts right after the previous field. offset(0) means the same.

offset only works when both of these are true:

  • Every field has length. If one field has no length, Precept ignores the whole layout, including every offset, and shows no error when you import the module. The editor shows a warning on the first field that has no length.
  • A fixed-length file rule matches the file. Turn on Fixed-length in the file read rule (fixedLength: true). Without it, Precept reads the file as delimited text and does not use length or offset. If the file rule sets skipChars, that number is the default gap for every field except the first. An offset on a field replaces it.

Layout specifications often list the absolute start position of each field, counted from 0. Change each position to a gap with this rule: gap = start - end of the previous field. The end of a field is its start plus its length. The gap of the first field is its start position.

FieldStartLengthPrevious field ends atAnnotation
custodianID010(start of the line)length(10) offset(0)
accountNumber111210length(12) offset(1)
quantity24823length(8) offset(1)
price401032length(10) offset(8)
pdm
vendor acme
name positions

model Position {
  custodianID: string length(10) offset(0)
  accountNumber: string length(12) offset(1)
  quantity: number length(8) offset(1)
  price: number length(10) offset(8)
}

accountNumber starts at position 11, but its annotation is offset(1), not offset(11). The previous field ends at position 10, so Precept skips one character. If you write the absolute position by mistake, every later field moves to the right.

Actions ​

An action reads or writes data in an external system. Put actions in a resource block inside a model.

  • A query reads data.
  • A mutation changes data.
pdm
vendor acme
name actions

defaults {
  http {
    baseUrl: "https://api.example.com"
  }
}

config {
  apiKey: secret string
}

model Contact {
  id: string
  name: string

  resource {
    query list(): Contact[] -> {
      http.get("/contacts")
    }

    query get(id: string): Contact -> {
      http.get(`/contacts/{id}`)
    }

    query search(name?: string, limit?: number = 50): Contact[] -> {
      http.get("/contacts", {
        query: {
          name: name,
          limit: limit
        }
      })
    }

    mutation create(name: string): Contact -> {
      http.post("/contacts", {
        headers: {
          "X-Api-Key": `{config.apiKey}`
        },
        body: {
          name: name
        }
      })
    }
  }
}

Parameters ​

  • The form of a parameter is name: type.
  • Put ? after the name to make a parameter optional.
  • Put = value after the type to give a default value.
  • The type after ): is the return type. Put [] after it if the action returns a list.

Declarative actions ​

A declarative action has -> and a block with one HTTP call. The call is http.get, http.post, http.put, http.patch or http.delete.

  • The first argument is the path. Precept adds the path to baseUrl from defaults.
  • In a string in backticks, {id} inserts the parameter id. {config.apiKey} inserts a configuration value.
  • The second argument is optional. It can have headers, query and body.

Script actions ​

A script action holds JavaScript between {= and =}. The script runs as an async function. The script can use config, each parameter by name, and fetch. The script must return the result.

pdm
vendor acme
name scripts

config {
  baseUrl: string
}

model Post {
  id: number
  title: string

  resource {
    query get(id: number): Post {=
      const response = await fetch(`${config.baseUrl}/posts/${id}`)
      return response.json()
    =}
  }
}

Run an action ​

Precept makes each action available at this path:

/rest/{vendor}/{name}/resource/{model}/{action}

For example, the get action above is at GET /rest/acme/scripts/resource/Post/get?id=1.

Mappings ​

A module can convert data from one model to another model in two ways:

  • A mapping block holds declarative rules that work in both directions.
  • A mapFn holds JavaScript that works in one direction.
pdm
vendor acme
name mappings

model Contact {
  first_name: string
  last_name: string
}

model Person {
  firstName: string
  lastName: string
}

mapping Contact <-> Person {
  first_name <-> firstName
  last_name <-> lastName
}

For the rules and the functions, see Mapping Engine. For complete examples, see Mapping Examples.