Skip to content

How table definitions work

Table definitions are managed from the admin panel’s three-pane browser. Once a profile and table are selected there, five dedicated pages carry out the individual actions. Creating a table is a separate builder page, because its form is long enough to deserve its own screen.

Every page here requires the struct:table manage permission. A missing session redirects to /login; a session without the permission receives 403 with a table-management message.

Action Route Page
Add columns to a table GET/POST /admin/tables/columns Columns and appending
Rename a column POST /admin/tables/rename Renaming columns
Delete a table GET/POST /admin/tables/delete Deleting tables
Copy a profile GET/POST /admin/profiles/copy Copying profiles
Generate from an invoice template GET/POST /admin/tables/from-template Invoice templates
Read rename history GET /admin/profiles/history Rename history

The old combined /admin/table-definition workspace responds with a permanent redirect to /admin.

The table-scoped pages (columns, delete, history) are entered with ?profile=<name>&table=<name>. The profile-scoped pages (copy, template) drop the table, because they act on a whole profile. The write forms carry the selection in hidden fields, since a write answers with the page re-read from the backend rather than whatever the browser still had on screen.

After a successful write, each page re-reads itself from the backend and shows the definition as it now is — not the form you submitted. A refused write answers the same way but with 422 and the backend’s own message.

A profile’s own tables come from the profile tree; the Global scope’s tables come from the table catalog. The Global scope is labeled “Global — all profiles” in page headings instead of its internal name __global.

System tables are backend-managed. The server refuses every structural write on them, so the pages do not offer the write panels for a system table.

The add-column panel and the create-table builder share one vocabulary, read live from the backend’s ListColumnTypes endpoint. A type the server adds is offered by both screens without a change here, and a type it stops accepting disappears from both.

Notable rules of that vocabulary:

  • Groups collapse related types into one picker choice — temporal for the date/time types and gtin for the GTIN lengths — resolved by a follow-up field.
  • Parameterised types must spell their arguments, such as decimal(12,3).
  • Link types must name their target, such as link(customer).
  • Creation-only types (accounting, accounting_transfer) can be chosen only while a table is being created, not when appending to an existing table.
  • Compound types are definition rows that expand into schema-managed companion columns and leave no column of their own name behind; they cannot be indexed or identify a row.
  • Currency and quantity-ledger rules come from the response, so money and accounting require a currency, while quantity-ledger is limited to the types the server marks as allowing it.

System column names (id, deleted, created_at, row_revision) stay reserved for user columns too, because they share one namespace in a data request.