Connectors
A connector defines how Liberty Next reaches a data source. Two kinds:
- SQL connector — a set of named SQL queries that run against a database pool. The schema of each result is discovered at runtime from the query cursor; display details (labels, formats, display rules) come from the dictionary.
- API connector — a set of named HTTP endpoints. Authentication, base URL and placeholder substitution are configured on the connector.
Connectors are defined in config/connectors.toml. The file is hot-reloadable — POST /admin/reload rebuilds the registry while in-flight requests keep the version they started with.
At a glance
schemas mapSQL connector
Pool
A SQL connector points at a pool — one SQLAlchemy async engine.
[pools.myapp]
url = "postgresql+asyncpg://myapp@db:5432/myapp"
password = "ENC:…" # decrypted via [crypto] master_key — or a ${ENV} ref / plaintext
pool_size = 10
max_rows = 5000 # default SELECT row cap for this pool
dialect = "postgresql" # optional override; otherwise derived from the URL
[pools.myapp.schemas]
PROD = "myapp_prod" # `#SCHEMA.PROD#` in a query → `myapp_prod` at execute time
The pool's password may be carried separately from the URL (cleaner, keeps it out of logs) or embedded as ENC:… inside the URL. Either way the engine is built with URL-safe escaping. A wrong / missing master key when the password is ENC: keeps the value as-is and logs a warning.
Engines are created lazily — an unreachable DB never blocks startup; tests inject their own engine.
Connector
[connectors.myapp]
type = "sql"
pool = "myapp"
licensed = false # set true to gate behind the license key
max_rows = 5000 # overrides the pool default
Queries
A connector carries an ordered list of named queries.
[[connectors.myapp.queries]]
name = "users_get"
label = "Users" # tab title in the React UI
description = "Application users"
auto_load = true # run a SELECT on screen open
sql = "SELECT id, name, status FROM users ORDER BY name"
columns = [
{ name = "id", filter = true },
{ name = "name" },
{ name = "status", dd = "USER_STATUS" },
]
| Field | Description |
|---|---|
name | The connector-scoped name. Permissions reference it as sql:<connector>:<name>. |
label / description | Display names. The React UI titles the TableView with description, else label, else the menu label. |
sql | The SQL text — string, or a per-dialect map: sql = { default = "…", oracle = "…" } keyed by the SQLAlchemy backend name. A default is required. |
writable | true for non-SELECT statements. Combined with the caller's sql:<c>:<name> permission. |
auto_load | Runs the SELECT immediately on screen open instead of waiting for a Run click. |
max_rows | Per-query SELECT row cap. Overrides connector → pool → global default (1000). |
key_columns | Result columns that identify a row. Surfaced in describe() for the TableView's Excel-import update-vs-insert match. |
columns | Optional display hints — see Column hints below. |
params | Optional list of ParamDef — declares each :name the query expects, with a default, a dd for the input widget and a label. |
Column hints
A columns entry augments the discovered schema; it does not replace it. Anything omitted comes from cursor.description.
columns = [
{ name = "id", hidden = true, filter = true },
{ name = "status", dd = "USER_STATUS", width = 120 },
{ name = "amount", format = "amount", align = "right" },
{ name = "score", visible_when = { field = "view_mode", value = "advanced" } },
]
| Hint | Effect |
|---|---|
dd | Dictionary entry key — pulls label, format and the BOOLEAN / ENUM / LOOKUP rule. dd = "" opts out. |
label, format | Per-column override when the dictionary entry is not enough. |
hidden | Drops the column from the grid (stays available for filters and forms). |
filter | Adds the column to the per-column filter row above the grid. |
filter_from | List of { source, column } — cascading-filter deps. When source has a value, this column's LOOKUP options narrow to the lookup rows whose column matches it. |
visible_when | A { field, value } rule (or a list, all AND-ed). The column is dropped entirely when a rule does not pass. |
width, align | Grid layout hints. |
Statement gates
Every statement is parsed and classified before binding:
- Allowed:
SELECT,INSERT,UPDATE,DELETE,MERGE. AWITH … SELECTresolves toSELECT; aWITH … DELETEresolves toDELETE. An unparseableWITH→ rejected. - Writable gate:
INSERT/UPDATE/DELETE/MERGErequirewritable = trueand the caller'ssql:<c>:<name>permission. Either side missing →403.
:name → ? binding
Every :name token in the SQL is rewritten to a positional ? and bound via PreparedStatement. Values are never string-substituted. The parser respects:
- single-quote string literals (
'O''Brien'), - double-quoted identifiers (
"customer.name"), - line and block comments (
-- …//* … */), - the PostgreSQL
::typecast operator ('foo'::text).
A :name the caller omits binds to SQL NULL — keeps the same query usable in create and update paths.
#SCHEMA.X# placeholders
A query may reference #SCHEMA.PROD# (or any other key) in its SQL. At execute time the placeholder is replaced from the pool's schemas map — PROD = "myapp_prod" → myapp_prod. A #SCHEMA.X# with no mapping (or a mapping that is not a plain identifier) raises ConnectorError. Right for dev / prod schema swaps and for several schemas under one DB user.
params and lookup_param_binds
Declared on the query so the React form layer knows what to ask for. A params entry can carry a dd so the widget is dictionary-driven (BOOLEAN → checkbox, ENUM → searchable dropdown, LOOKUP → searchable dropdown). LOOKUP-type entries can also reference earlier form values via lookup_param_binds — a value literal or a source reading the live form state — so a UDC-style WHERE narrows correctly.
API connector
[connectors.myservice]
type = "api"
base_url = "https://api.example.com"
auth = "bearer" # none / basic / bearer / api_key / oauth2
auth_token = "ENC:…" # decrypted at runtime; ${ENV} ref also accepted
[[connectors.myservice.endpoints]]
name = "ping"
method = "GET"
path = "/health"
[[connectors.myservice.endpoints]]
name = "lookup_user"
method = "GET"
path = "/users/{{user_id}}"
| Field | Description |
|---|---|
auth | none / basic (auth_user + auth_pass) / bearer (auth_token) / api_key / oauth2. |
| OAuth2 | Token-endpoint POST + dot-path extraction + TTL cache + one refresh on 401. Body is form-encoded or JSON depending on auth_token_content_type. |
{{placeholder}} | Substituted in the path, query string and body from the call's parameters. |
endpoint.response | Dot-path map for the response — surfaces named values to action chains via {call.N.fieldName}. |
writable | An endpoint emitting POST / PUT / DELETE needs writable = true. |
Endpoints emit POST /api/http/{connector}/{endpoint} and are gated by api:<connector>:<endpoint>.
Hot reload
Edit connectors.toml, then call POST /admin/reload (superuser only). The framework rebuilds ConnectorRegistry, re-reads the dictionary and the menus, swaps them on app.state and disposes the previous registry. In-flight requests keep the version they started with — no race on a query mid-execute. The AI assistant's connector tools refresh on JVM restart, not on reload.
The same admin route surfaces in the React Settings tab — every config builder writes through PUT /admin/config/<pools|connectors|dictionary|menus|screens> and then prompts a Reload.
Tips & best practices
- Discover, do not declare. Let
cursor.descriptiondrive the schema. Usecolumnshints only for what the cursor cannot tell you (label, format, visibility, cascading filter). - Dictionary entries belong on the dictionary, not on every query. Define
USER_STATUSonce under[entries.USER_STATUS]; reference it withdd = "USER_STATUS"from any query that returns it. - Per-dialect SQL only when needed. A query that works on every backend stays a single string. Use the map form only for Oracle-specific syntax or a function that differs across backends.
- Keep the pool password out of the URL. A
password = "ENC:…"(or${ENV}ref) lives next to the URL — easier to rotate, never logged as part of the connection string. - Always set
writable = trueon mutating queries. A typo in the connector permission is still caught by the gate at execute time, but the TOML flag is the right place to declare intent. max_rowsfloors deep: per-request override → query → connector → pool → global default (1000). Set a sensible per-query value when an operator commonly wants the whole table.