aster.Web Data / Docs Aster UI ↗

Aster Web Data / Connect your data

Model your data

Describe each set in a YAML DataSource: its fields and types, keys, relations, text collation and generated values.

Every set a service publishes starts as a DataSource: a short YAML file that names the set, the table behind it and the fields a client may see. This one publishes a customers table as /Customer:

dataSource:
  ID: Customer
  serverType: sql
  tableName: svc_customers
  fields:
    - {name: id, type: sequence, primaryKey: true}
    - {name: name, type: text, length: 100}
    - {name: email, type: text, length: 200}

From these lines Aster builds the set's entry in $metadata and the OpenAPI description, the query options on each field, and the checks on each write. A change to the file changes all of them together, and the server picks it up on reload.

ID is the set's name in the API. tableName names the table and defaults to the ID, and a field's nativeName names its column and defaults to the field's name. serverType: sql reads a table, and serverType: virtual reads rows from a script instead. A service lists its DataSource files, or directories of them, under datasources:.

Aster does not infer the model from the database. A table records how the data is stored, and the file states what it means to the API. Connect your own database generates a starting point from an existing schema and checks it against the database.

Fields and types

Each field has a name and a type:

Type Holds Declare
text strings length, the longest value the field keeps
integer whole numbers, 32-bit (Edm.Int32)
long whole numbers, 64-bit (Edm.Int64)
sequence 64-bit keys that the database generates (see Keys)
decimal exact decimal numbers (Edm.Decimal) precision and scale
float binary floating-point numbers (Edm.Double)
boolean true or false
date a calendar date
datetime a date and time with an offset (Edm.DateTimeOffset) precision, the fractional-second digits
time a time of day (Edm.TimeOfDay) precision, the fractional-second digits
uint128 unsigned 128-bit integers, sent as exact decimal text

A decimal without precision and scale takes the database's own type: an unbounded NUMERIC on PostgreSQL, and DECIMAL(65,30) on MariaDB. Declare both facets for the values the field stores:

    - {name: amount, type: decimal, precision: 12, scale: 2}

A write that goes beyond the declared precision or scale is refused with 400, naming the property and the limit. Aster never rounds the value. A time or timestamp with more fractional digits than its precision is refused in the same way.

Set edmType on a field to choose its OData type when the field type does not give one. The scaffold does this for UUID and blob columns, with Edm.Guid and Edm.Binary.

At start-up, Aster compares each column with its field. A column that stores something other than the field declares fails the check, and the message says what to declare.

Keys

Mark a field primaryKey: true. Two or more fields marked this way make a composite key, which the link table in Relations uses.

A sequence key is generated by the database. When Aster creates the table itself with setup: recreate, the column is an AUTO_INCREMENT column on MariaDB or an identity column on PostgreSQL. An in-memory set assigns the keys from a counter. Scaffolded generated columns are written with canSave: false, so clients cannot write them.

Relations

Put foreignKey: <DataSource>.<field> on the field that refers to another set's key:

# billing/Invoice.ds.yaml
dataSource:
  ID: Invoice
  serverType: sql
  tableName: svc_invoices
  fields:
    - {name: id, type: sequence, primaryKey: true}
    - {name: customerId, type: integer, foreignKey: Customer.id}
    - {name: amount, type: decimal, precision: 12, scale: 2}

Aster builds two navigations from one foreign key. The referencing set gets a single navigation named after the field, without its Id suffix: customer for customerId. The referenced set gets a collection named after the referencing set, with a lower-case first letter and an s: invoices for Invoice. Set inverseName on the field to choose the collection's name. Requests use the navigations in $expand and in paths:

GET /shop/Invoice
expandcustomer
selectname
As a URLGET /shop/Invoice?$expand=customer($select=name)
GET /shop/Customer(1)/invoices
As a URLGET /shop/Customer(1)/invoices

A many-to-many relation needs a link set. Mark the set linkTable: true. It has exactly two fields, both primary keys, and each one refers to the one-field key of a DataSource of the same type. Each field's linkName names the navigation on the set it refers to, which lists the entities the other field refers to. Both sides get a collection, and the link set is not published as a set of its own.

dataSource:
  ID: ProductRelation
  serverType: sql
  tableName: product_relations
  linkTable: true
  fields:
    - {name: productId, type: integer, primaryKey: true, foreignKey: Product.id, linkName: relatedProducts}
    - {name: relatedProductId, type: integer, primaryKey: true, foreignKey: Product.id, linkName: relatedFrom}

Text collation

A comparison, sort or LIKE on a text field uses the column's own collation, and Aster publishes it as AsterData.Collation. A case-insensitive column therefore compares case-insensitively, as your database does. Where Aster computes the result itself, such as a sort across two databases, it uses code point order, which is case sensitive. The AsterData vocabulary lists the case-insensitive functions that use the column's collation.

Declare a collation on a text field with collation:, using the database's own name for it, such as C on PostgreSQL or utf8mb4_bin on MariaDB:

    - {name: email, type: text, length: 200, collation: C}

A database can declare a collation for every text field that names none of its own. Aster publishes the declared collation as AsterData.Collation and checks it against each column. A column with another collation fails the start-up check. A text field that names none of its own takes the database's collation, else the column's own, which Aster reads from the database when it connects.

A virtual data source declares its collation in its pushdown: block, with collation: ordinal. See Virtual data sources.

Defaults and generated values

defaultValue sets the value an insert stores when the request leaves the field out:

    - {name: status, type: text, length: 20, defaultValue: open}

An explicit null in the request is kept. An update never applies a default. Scaffold writes a literal default from the database as defaultValue too.

A field can be computed from the record it belongs to, or from a related record. template builds text from the record's other fields. includeFrom reads a field of the record that a foreign key refers to. A record without a related record is still read.

    - {name: categoryName, type: text, includeFrom: Category.name}
    - {name: label, type: text, template: "#{code}: #{name}"}

Aster can keep a version number for a set, and the set's ETag comes from it. Name an integer or long field with versionField. Aster sets it to 1 on create and adds 1 on every update. A client cannot write it, and two updates that read the same version cannot both succeed.

dataSource:
  ID: Order
  serverType: sql
  tableName: orders
  versionField: Version   # the field list is as for any other set

A set's concurrency token is what its ETag carries. Declare it with etag:

dataSource:
  ID: Order
  serverType: sql
  tableName: orders
  etag: {columns: [version]}          # the values of these columns
  # etag: native                      # the database's own row version (PostgreSQL: xmin)
  # etag: {sql: "audit.row_version({row})"}   # an expression; {row} is the row, {name} a column
  # etag: none                        # no ETag: the last write wins

PostgreSQL sets use native unless they declare another token. Other databases have no row version of their own, so a set there declares a version column (versionField or etag: {columns: [...]}) or an expression; aster-webdata-server check lists the sets that have no token. A token column must be readable by everyone who can read the set, because the tag carries its value; that holds for the columns an sql expression names too, and the server refuses to load a set whose token reads a column some reader cannot see. $metadata lists the columns of a columns token in Core.OptimisticConcurrency, and an empty list for native and sql.

Next