Connectors
General logic
The connectors is distributed as a separate package and loaded by AdminForth automatically. You do not need to instantiate or import the connector manually in your app config.
AdminForth resolves connectors by datasource URL scheme, so when it sees sqlite://... it tries to load @adminforth/connector-sqlite.
Setup
SQLite
pnpm add @adminforth/connector-sqlite
That is enough for connector wiring because connectors are integrated through the peer dependency system.
Postgres
pnpm add @adminforth/connector-postgres
MySQL
pnpm add @adminforth/connector-mysql
Clickhouse
pnpm add @adminforth/connector-clickhouse
Mongo
pnpm add @adminforth/connector-congo
Qdrant
pnpm add @adminforth/connector-qdrant
How peer dependency loading works
AdminForth keeps connectors optional and attempts to import only the connector that matches your datasource type.
For SQLite this means:
- You set datasource URL to
sqlite://... - AdminForth attempts to import
@adminforth/connector-sqlite - Connector is used automatically for schema discovery and CRUD operations
No extra connector registration is required in the usual setup.
Composite primary keys in connectors
Resources can have several columns marked with primaryKey: true (see
Composite primary keys). Core encodes values of all such
columns into single recordId string and decodes it back for connectors, but connector still has to build
WHERE clause over several columns.
Connector declares support with one field and uses pkValues argument which base connector passes to it:
export default class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
supportsCompositePrimaryKey = true;
async updateRecordOriginalValues({ resource, recordId, newValues, pkValues }) {
const pkEntries = Object.entries(pkValues ?? { [this.getPrimaryKey(resource)]: recordId });
const setClause = Object.keys(newValues).map((col, i) => `"${col}" = $${i + 1}`).join(', ');
const whereClause = pkEntries
.map(([col], i) => `"${col}" = $${Object.keys(newValues).length + i + 1}`)
.join(' AND ');
await this.client.query(
`UPDATE ${resource.table} SET ${setClause} WHERE ${whereClause}`,
[...Object.values(newValues), ...pkEntries.map(([, value]) => value)],
);
}
async deleteRecord({ resource, recordId, pkValues }): Promise<boolean> {
const pkEntries = Object.entries(pkValues ?? { [this.getPrimaryKey(resource)]: recordId });
const whereClause = pkEntries.map(([col], i) => `"${col}" = $${i + 1}`).join(' AND ');
const res = await this.client.query(
`DELETE FROM ${resource.table} WHERE ${whereClause}`,
pkEntries.map(([, value]) => value),
);
return res.rowCount > 0;
}
}
Notes:
pkValuesis a map of primary key column name to value already casted withsetFieldValue, and it works for single primary key resources too, so the same code path serves both cases.- Optional
deleteMany({ resource, recordIds })should buildORof per-recordANDconditions for composite keys, e.g.WHERE (a = $1 AND b = $2) OR (a = $3 AND b = $4). Usethis.getPrimaryKeyValues(resource, recordId)to split each record id. discoverFieldsshould mark every column of table primary key withprimaryKey: true(in Postgres these are columns of thePRIMARY KEYconstraint), otherwise users have to set it manually in resource config.
If connector does not set supportsCompositePrimaryKey, AdminForth throws on startup when it meets a resource
with composite primary key, instead of silently updating or deleting wrong rows.