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:
vendorandname(required)version(optional)meta(optional)defaults(optional)- All other parts, in any order:
import,config,model,type,mappingandmapFn
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
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
}Header
The header names the module. vendor and name together identify the module. The values can be plain identifiers or quoted strings.
vendor acme
name crm
version "1.0"
meta {
productName: "Acme CRM"
description: "Contacts from the Acme CRM API"
}| Keyword | Required | Description |
|---|---|---|
vendor | Yes | The organization that owns the module |
name | Yes | The name of the module |
version | No | The version of the module |
meta | No | Key-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>.
vendor acme
name settings
config {
baseUrl: string
username: string
password: secret string
retries?: number
}- Put
?after the name to make a value optional. - Put
secretbefore 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.
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.
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.
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
| Type | Example | Description |
|---|---|---|
string | name: string | Text |
number | age: number | A number |
boolean | active: boolean | true or false |
| A model | address: Address | A record of another model |
| An array | tags: string[] | A list of values of one type |
| A literal | kind: "person" | Exactly this value |
| A union | status: "active" | "inactive" | One of the types in the list |
| A named type | status: Status | A 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.
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.
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:
| Annotation | Argument | Description |
|---|---|---|
primary | None | The field identifies the record. |
unique | None | No two records have the same value. |
readonly | None | The field is read-only. |
secret | None | The value is sensitive. |
title | Text | A short label for the field |
description | Text | A longer description of the field |
example | Text | A sample value |
default | Text | The default value of the field |
pii | Text | The field holds personal data. The argument names the kind of data. |
format | Text | The format of a string: "email", "url", "uuid", "datetime", "date" or "ip" |
length | Number | The exact number of characters. See length. |
offset | Number | The number of characters to skip after the previous field. See offset. |
maxLength | Number | The maximum number of characters |
minLength | Number | The minimum number of characters |
precision | Number | The maximum number of digits after the decimal point |
range | min: number, max: number | The lowest and highest value of a number |
relation | Identifier | The field refers to another model. |
file | Text | The 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.
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
lengthannotation. - Validation. When response validation is on, a string value must have exactly
ncharacters.
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 nolength, Precept ignores the whole layout, including everyoffset, and shows no error when you import the module. The editor shows a warning on the first field that has nolength. - 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 uselengthoroffset. If the file rule setsskipChars, that number is the default gap for every field except the first. Anoffseton 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.
| Field | Start | Length | Previous field ends at | Annotation |
|---|---|---|---|---|
custodianID | 0 | 10 | (start of the line) | length(10) offset(0) |
accountNumber | 11 | 12 | 10 | length(12) offset(1) |
quantity | 24 | 8 | 23 | length(8) offset(1) |
price | 40 | 10 | 32 | length(10) offset(8) |
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
queryreads data. - A
mutationchanges data.
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
= valueafter 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
baseUrlfromdefaults. - In a string in backticks,
{id}inserts the parameterid.{config.apiKey}inserts a configuration value. - The second argument is optional. It can have
headers,queryandbody.
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.
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
mappingblock holds declarative rules that work in both directions. - A
mapFnholds JavaScript that works in one direction.
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.