Aster Web Data / How it works
Change the model, not the code
What a new field, relation or rule changes in the API, how check confirms it against the database, and how a reload applies it without a restart.
The API, its $metadata, its OpenAPI description and its input validation are all built from the model: one DataSource file per set, and a rules file. When a screen needs a new field, a link to related records or a business rule, you edit the model, run check, and send the server SIGHUP. The parts of the API that depend on the change follow it, with no endpoint code to update and no restart.
Add a field
The sales team wants each customer's region on their customer list, with a filter and a sort by it. The customers table already has the column, so the change is one line in the DataSource:
# shop/Customer.ds.yaml
dataSource:
ID: Customer
serverType: sql
tableName: customers
fields:
- {name: id, type: sequence, primaryKey: true}
- {name: name, type: text, length: 100, required: true}
- {name: email, type: text, length: 200}
- {name: region, type: text, length: 8} # the new field
A field may be empty unless it declares required: true. After a reload, $metadata has the new property:
</Record>
</Annotation>
</Property>
+ <Property Name="region" Type="Edm.String" MaxLength="8">
+ <Annotation Term="AsterData.Collation">
+ <Record>
+ <PropertyValue Property="Kind" String="locale"/>
+ <PropertyValue Property="Name" String="en_US.UTF-8"/>
+ </Record>
+ </Annotation>
+ </Property>
<NavigationProperty Name="invoices" Type="Collection(Shop.Invoice)" Partner="customer"/>
</EntityType>
<EntityType Name="Invoice">
Everything else that reads the model changes with it:
| Part | What changes |
|---|---|
| OpenAPI | The schema for Customer at <root>/openapi.json includes region. |
| Queries | $select, $filter and $orderby accept region. |
| Writes | Inserts and updates accept region, and refuse a value longer than 8 characters. |
| Aster UI | Grids and forms read the published metadata, so region gets a text editor and a text filter. |
A client can use the field at once:
| select | name,region |
|---|---|
| filter | region ne 'EU' |
| orderby | region,name |
| top | 5 |
As a URL
GET /shop/Customer?$select=name,region&$filter=region ne 'EU'&$orderby=region,name&$top=5HTTP 200
content-type: application/json;odata.metadata=minimal
odata-version: 4.01The first 3 of 5 rows
{
"@odata.context": "http://localhost:8080/shop/$metadata#Customer(name,region,id)",
"value": [
{
"@odata.etag": "W/\"54706134\"",
"name": "Dune Works",
"region": "APAC",
"id": 8
},
{
"@odata.etag": "W/\"54706134\"",
"name": "Elm Supply",
"region": "APAC",
"id": 9
},
{
"@odata.etag": "W/\"54706134\"",
"name": "Fern Supply",
"region": "APAC",
"id": 11
}
]
}A field the model does not publish cannot be selected, filtered or sorted on, so the API exposes only the columns you have chosen.
Add a relation
A field that refers to another set becomes a navigation. In the shop, each invoice names its customer:
- {name: customerId, type: integer, required: true, foreignKey: Customer.id, inverseName: invoices}
That one line gives each invoice a customer and each customer an invoices list, which clients can expand, filter by and sort by. This request reads invoices with their customer's name:
| expand | customer |
|---|---|
| select | name |
As a URL
GET /shop/Invoice?$expand=customer($select=name)Model your data covers the other kinds of relation, including many-to-many through a link table.
Add a rule
A rule in the rules file applies to every request that writes the set, from any client. This one refuses an invoice whose amount is not positive:
rules:
- id: PositiveAmount
entity: Invoice
host: Entity
kind: Constraint
condition: "amount gt 0"
message: An invoice amount must be more than zero
An insert with a negative amount is then refused before anything is written:
HTTP 400
content-language: en
content-type: application/json;odata.metadata=minimal
odata-version: 4.01{
"error": {
"code": "ValidationFailed",
"message": "An invoice amount must be more than zero",
"details": [
{
"code": "PositiveAmount",
"message": "An invoice amount must be more than zero"
}
]
}
}Rules describes the kinds of rule and what a condition can read.
Check the model against the database
aster-webdata-server check --config FILE compares every DataSource with the table it names: that the table and each column exist, and that each column stores what the field declares, including precision, scale, collation and default. If the model declares region and the table has no such column, it says so:
$ aster-webdata-server check --config server.yaml
shop.Customer: shop.public.customers
shop.Invoice: shop.public.invoices
shop.InvoiceLine: shop.public.invoice_lines
shop.Product: shop.public.products
databases.shop: 1 mismatch(es)
table `public.customers` has no column `region`
With the column in place, it passes:
$ aster-webdata-server check --config server.yaml
shop.Customer: shop.public.customers
shop.Invoice: shop.public.invoices
shop.InvoiceLine: shop.public.invoice_lines
shop.Product: shop.public.products
databases.shop: ok
It exits with 1 on a mismatch and 2 on an invalid configuration, so a deployment pipeline can stop a release on either. A timestamp column whose precision the field leaves out is reported with the line to add.
On a database with no row version of its own, check also warns about each set that declares no concurrency token, because a write to it overwrites a concurrent change. Declare an etag, or etag: none to say that is intended. A warning does not change the exit code.
The server runs the same check at start-up and on every reload. A database that fails it is unavailable: the sets stored on it answer 503 with Retry-After, and every other set keeps serving. Set required: true on the database to stop the process instead.
Reload without a restart
Send the server SIGHUP and it reads the configuration and the model again, builds and checks the new service as it would at start-up, and serves new requests with it. Requests already running finish on the old service, so no client sees an error, and readiness stays true throughout.
kill -HUP "$(pidof aster-webdata-server)"
If anything in the new configuration or model fails, the old service keeps serving. The log line reload failed gives the reason, and the counter aster_webdata_config_reloads_total{outcome="failed"} goes up. A database that cannot be reached is not a failure: as at start-up, its sets answer 503 until it returns, unless it is required.
A database whose connection settings did not change keeps its open connections, even when the model adds sets to it. A reload never runs setup or loads a seed, so it cannot recreate a live database. One case needs a restart: a database with setup: recreate that would need new tables refuses the reload, and the old service keeps serving.
Under systemd, give the unit a reload command:
[Service]
ExecStart=/usr/local/bin/aster-webdata-server --config /etc/shop/server.yaml
ExecReload=/bin/kill -HUP $MAINPID
systemctl reload shop-odata then applies a change. In Kubernetes, update the mounted configuration and model files, then signal the process: kubectl exec deploy/shop-odata -- kill -HUP 1. A new image or a changed environment still needs a rolling restart.
A few keys belong to the running process, such as listen and log, and take effect at the next restart; the reload applies everything else and logs a warning naming each one. Configure and deploy lists them. Windows has no SIGHUP, so on Windows a change needs a restart.
Migrations and access
A model edit does not change the database, and it does not decide who may use the field:
- The column. The table must already have the column the model names. Aster Web Data does not alter tables on an existing database, so write the migration with your database's own tools, then run
check. - Access. Decide who may read and write the new field, as you did for the set's other fields. Access is set per set in the service configuration, and a rule can make a field read-only or required under a condition. See Authentication and access.
Clients see the new field when they next read the metadata. Ship the migration and the model change in the same release, so the database, the model and the client code move together.