Skip to content

How the studio works

A description of what happens between a browser request and the SQL the studio runs. Useful when a field renders unexpectedly, a save fails, or you want to predict the effect of a register option.

  • Debugging a form field type or a validation message.
  • Understanding which columns are written on add and edit.
  • Checking how expensive list pages are on large tables.

For configuration options, go straight to the GisilaStudio reference.

handler(prefix: p) returns a Shelf handler with these routes, relative to the mount point. <table> is the SQL table name (TableMeta.tableName), <pk> the primary key value.

Method and path Page or action
GET / Dashboard: registered models grouped by group, with row counts
GET /<table>/ List: ?q= search, ?page= pagination
GET /<table>/add/ Add form
POST /<table>/add/ Insert
GET /<table>/<pk>/change/ Edit form
POST /<table>/<pk>/change/ Update
GET /<table>/<pk>/delete/ Delete confirmation
POST /<table>/<pk>/delete/ Delete
POST /<table>/action/ Bulk action (action=delete with ids)
GET /login/, POST /login/ Sign-in (only with auth)
POST /logout/ Sign-out (only with auth)

A table that is not registered answers 404. prefix is used to build every link and redirect, so it must equal the mount path.

The studio combines two sources:

  • TableMeta from gisila_orm gives the table name, the ordered column names and the primary key. Only columns in TableMeta.columnNames appear on forms.
  • Postgres information_schema.columns (schema public) gives each column’s data_type, nullability and default. This is queried once per table and cached for the life of the process, so restart the server after a migration that changes column types.

Input types follow the Postgres type:

Postgres type Input
integer, bigint, smallint, serial, bigserial number
real, double precision, numeric, decimal number with decimals
boolean checkbox
date date
time with[out] time zone time
timestamp with[out] time zone datetime-local
text, json, jsonb textarea
anything else text

A column in passwordFields is always a password input. A column in readonlyFields is shown as text. A field is marked required when the column is NOT NULL, has no default and is not read-only (and, on edit, is not a password field).

On add and edit the studio walks TableMeta.columnNames and, for each column that is editable (not in readonlyFields or excludeFields) and is not the primary key, parses the submitted value:

  • boolean: checked (on, true or 1) is true, anything else (including unchecked) is false.
  • blank value: NULL if the column is nullable; left out of the write (so the database default applies, or on edit the stored value stays) if it has a default; '' for text columns; otherwise the error “This field is required.”
  • numbers and dates: parsed; a value that does not parse is a field error (“Enter a whole number.”, “Enter a number.”, “Enter a valid date and time.”).
  • edit, password field, blank: skipped, so the stored value stays.

Then the onSave hooks run for columns that have a value, autoNowOnUpdateFields are set to the current UTC time, and the studio runs INSERT ... RETURNING * or UPDATE ... WHERE pk = $n. Field errors re-render the form with status 422; a database error re-renders it with status 500 and the first line of the error.

After saving, the buttons choose the redirect: back to the list, to the edit page (“save and continue”), or to a new add form (“save and add another”).

  • Columns: listDisplay, or the first six columns not in passwordFields.
  • Order: ordering (prefix - for descending), or the primary key ascending.
  • Search: with ?q=, every searchFields column that exists in the table is matched with "col"::text ILIKE '%q%', combined with OR.
  • Pagination: pageSize rows per page, LIMIT/OFFSET. page below 1 is treated as 1.

Each list request runs a COUNT(*) with the search filter plus the page query.

The sidebar and dashboard show a count per registered table on every page. A table that Postgres estimates (pg_class.reltuples) at 100,000 rows or more shows the estimate; smaller tables get an exact COUNT(*).

Single deletes go through the confirmation page. Bulk delete runs one DELETE ... WHERE pk IN (...) for the selected rows. Primary key values that look like integers are sent as integers, others as strings.

How the options map to the pages for one model:

studio.register<User>(
UserTable.metadata,
listDisplay: ['id', 'email', 'isActive', 'dateJoined'], // list columns
searchFields: ['email', 'firstName'], // ?q= targets
readonlyFields: ['id', 'dateJoined'], // shown, not editable
excludeFields: ['legacyFlags'], // not on forms
autoNowOnUpdateFields: ['updatedAt'], // stamped on save
ordering: ['-dateJoined'], // list order
pageSize: 50,
);