Skip to main content

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

📄 connectors.toml[pools.myapp]url = "postgresql+asyncpg://…"password = "ENC:…"max_rows = 5000[connectors.myapp]type = "sql"pool = "myapp"licensed = falsequeries = [...][[connectors.myapp.queries]]name = "users_get"label = "Users"auto_load = truesql = "SELECT id, name, status …"columns = [...]⚙ SQLConnectorexecute(name, params, lang)→ rows + Column[]label / format / rule resolvedwritable gateUPDATE / INSERT / DELETE → writable=true:name → ? bindnever string-substituted into SQL#SCHEMA.X# resolutionfrom pool's schemas mapper-dialect SQLsql = { default, oracle, postgresql }RESTPOST/api/query/{c}/{q}POST/api/http/{c}/{e}GET/api/lookup/…Permissionssql:{c}:{q}api:{c}:{e}ConsumersTableView gridDashboard panelsAI assistant toolsLookup picker

SQL 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" },
]
FieldDescription
nameThe connector-scoped name. Permissions reference it as sql:<connector>:<name>.
label / descriptionDisplay names. The React UI titles the TableView with description, else label, else the menu label.
sqlThe SQL text — string, or a per-dialect map: sql = { default = "…", oracle = "…" } keyed by the SQLAlchemy backend name. A default is required.
writabletrue for non-SELECT statements. Combined with the caller's sql:<c>:<name> permission.
auto_loadRuns the SELECT immediately on screen open instead of waiting for a Run click.
max_rowsPer-query SELECT row cap. Overrides connector → pool → global default (1000).
key_columnsResult columns that identify a row. Surfaced in describe() for the TableView's Excel-import update-vs-insert match.
columnsOptional display hints — see Column hints below.
paramsOptional 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" } },
]
HintEffect
ddDictionary entry key — pulls label, format and the BOOLEAN / ENUM / LOOKUP rule. dd = "" opts out.
label, formatPer-column override when the dictionary entry is not enough.
hiddenDrops the column from the grid (stays available for filters and forms).
filterAdds the column to the per-column filter row above the grid.
filter_fromList 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_whenA { field, value } rule (or a list, all AND-ed). The column is dropped entirely when a rule does not pass.
width, alignGrid layout hints.

Statement gates

Every statement is parsed and classified before binding:

  • Allowed: SELECT, INSERT, UPDATE, DELETE, MERGE. A WITH … SELECT resolves to SELECT; a WITH … DELETE resolves to DELETE. An unparseable WITH → rejected.
  • Writable gate: INSERT / UPDATE / DELETE / MERGE require writable = true and the caller's sql:<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 ::type cast 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}}"
FieldDescription
authnone / basic (auth_user + auth_pass) / bearer (auth_token) / api_key / oauth2.
OAuth2Token-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.responseDot-path map for the response — surfaces named values to action chains via {call.N.fieldName}.
writableAn 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.description drive the schema. Use columns hints 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_STATUS once under [entries.USER_STATUS]; reference it with dd = "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 = true on 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_rows floors deep: per-request override → query → connector → pool → global default (1000). Set a sensible per-query value when an operator commonly wants the whole table.