Summary
This document defines the v1 adapter interface for no-orm.
The adapter layer is responsible for exposing a generic persistence interface over concrete backends such as:
- SQLite
- Postgres
- MySQL
- MongoDB
- memory
This layer is generic and has nothing to do with any single host library or resource type.
Purpose
The adapter layer exists to:
- consume the canonical
Schema
- support minimal schema bootstrap through
migrate()
- expose a normalized persistence interface across SQL and non-SQL backends
- hide backend-specific mechanics behind a common contract
This layer does not own:
- request-aware policy enforcement
- schema extension hooks
- write/query scoping hooks
- host-library-specific logic
Those concerns belong in higher layers.
Design Principles
- The interface should be generic, not tied to a specific resource such as conversations.
- The interface should be method-oriented rather than SQL-oriented.
- The interface should be complete enough to cover standard CRUD-style persistence needs.
- The interface should work for both SQL and non-SQL backends.
- The interface should prefer generic operators such as
eq over backend-specific operators such as json_eq.
Interface
export interface Adapter {
migrate?(args: {
schema: Schema;
}): Promise<void>;
transaction?<T>(fn: (tx: Adapter) => Promise<T>): Promise<T>;
create<T = Record<string, unknown>>(args: {
model: string;
data: T;
select?: Select<T>;
}): Promise<T>;
update<T = Record<string, unknown>>(args: {
model: string;
where: Where<T>;
data: Partial<T>;
}): Promise<T | null>;
updateMany<T = Record<string, unknown>>(args: {
model: string;
where?: Where<T>;
data: Partial<T>;
}): Promise<number>;
upsert?<T = Record<string, unknown>>(args: {
model: string;
where: Where<T>;
create: T;
update: Partial<T>;
select?: Select<T>;
}): Promise<T>;
delete<T = Record<string, unknown>>(args: {
model: string;
where: Where<T>;
}): Promise<void>;
deleteMany?<T = Record<string, unknown>>(args: {
model: string;
where?: Where<T>;
}): Promise<number>;
find<T = Record<string, unknown>>(args: {
model: string;
where: Where<T>;
select?: Select<T>;
}): Promise<T | null>;
findMany<T = Record<string, unknown>>(args: {
model: string;
where?: Where<T>;
select?: Select<T>;
sortBy?: SortBy<T>[];
limit?: number;
offset?: number;
cursor?: Cursor<T>;
}): Promise<T[]>;
count?<T = Record<string, unknown>>(args: {
model: string;
where?: Where<T>;
}): Promise<number>;
}
Supporting Types
export type FieldName<T> = Extract<keyof T, string>;
export type Select<T> = ReadonlyArray<FieldName<T>>;
export type Where<T = Record<string, unknown>> =
| {
field: FieldName<T>;
op: "eq" | "ne";
value: unknown;
}
| {
field: FieldName<T>;
op: "gt" | "gte" | "lt" | "lte";
value: unknown;
}
| {
field: FieldName<T>;
op: "in" | "not_in";
value: unknown[];
}
| {
and: Where<T>[];
}
| {
or: Where<T>[];
};
export interface SortBy<T = Record<string, unknown>> {
field: FieldName<T>;
direction?: "asc" | "desc";
}
export interface Cursor<T = Record<string, unknown>> {
after: Partial<Record<FieldName<T>, unknown>>;
}
Method Semantics
migrate
migrate?(args: { schema: Schema }): Promise<void>
Purpose:
- prepare backend storage structures from the canonical schema
This is intentionally minimal:
- create model if not exists
- create indexes if needed
- use backend-specific idempotent DDL where supported
This is not a full migration framework.
transaction
transaction?<T>(fn: (tx: Adapter) => Promise<T>): Promise<T>
Purpose:
- allow multiple operations to run atomically where the backend supports transactions
Backends that do not support transactions may omit this method.
create
create<T>(args): Promise<T>
Purpose:
update
update<T>(args): Promise<T | null>
Purpose:
- update a single matching record
updateMany
updateMany?(args): Promise<number>
Purpose:
- update multiple matching records
- return the number of affected records
upsert
upsert?(args): Promise<T>
Purpose:
- create or update a record according to backend semantics
Optional because some backends may not support native or efficient upsert in v1.
delete
delete<T>(args): Promise<void>
Purpose:
- delete a single matching record
deleteMany
deleteMany?<T>(args): Promise<number>
Purpose:
- delete multiple matching records
- return the number of affected records
find
find<T>(args): Promise<T | null>
Purpose:
- find a single record matching the filter
findMany
findMany<T>(args): Promise<T[]>
Purpose:
- find multiple records
- support filtering, ordering, pagination, and optional projection
count
count?<T>(args): Promise<number>
Purpose:
Optional because it is not required by the current hebo-gateway benchmark, but included for completeness.
Where Semantics
The where model is intentionally small and generic.
Supported leaf operators in v1:
eq, ne
gt, gte, lt, lte
in, not_in
Supported logical composition in v1:
Notes:
- There is no dedicated
json_eq operator in v1.
- Backend-specific JSON path behavior should not leak into the generic filter model unless a strong use case proves it is necessary.
- If nested-path filtering becomes necessary later, it should likely be modeled with an optional path field rather than a separate operator family.
Ordering and Pagination
sortBy
Supports sorting by one or more fields.
cursor
The cursor shape is intentionally generic:
{
after: Partial<Record<FieldName<T>, unknown>>;
}
This allows backends to implement cursor pagination according to their own storage model while still constraining cursor fields to keys of T.
Why This Shape
This interface is based on the combined lessons from:
- the
hebo-gateway storage-layer discussion in issue #83
- Better Auth’s adapter interface
- unadapter’s normalized database API approach
The key decisions are:
- use a generic CRUD-style backend interface rather than a SQL executor interface
- keep backend concerns in this layer
- keep request-aware policy and schema extension outside this layer
- keep the filter and operation surface small and portable
Out of Scope
This spec does not define:
- schema extension hooks
- request-aware write/query enforcement
- host-library integration layers
- the TypeScript inference layer
- a full query language
- migration history or diffing
- backend capability metadata
Current Recommendation
Use this interface as the v1 adapter contract.
Start with:
- one SQL adapter, likely SQLite
- one non-SQL or non-database adapter, likely memory
That pair is enough to validate whether the interface is genuinely portable.
Summary
This document defines the v1 adapter interface for
no-orm.The adapter layer is responsible for exposing a generic persistence interface over concrete backends such as:
This layer is generic and has nothing to do with any single host library or resource type.
Purpose
The adapter layer exists to:
Schemamigrate()This layer does not own:
Those concerns belong in higher layers.
Design Principles
eqover backend-specific operators such asjson_eq.Interface
Supporting Types
Method Semantics
migratePurpose:
This is intentionally minimal:
This is not a full migration framework.
transactionPurpose:
Backends that do not support transactions may omit this method.
createPurpose:
updatePurpose:
updateManyPurpose:
upsertPurpose:
Optional because some backends may not support native or efficient upsert in v1.
deletePurpose:
deleteManyPurpose:
findPurpose:
findManyPurpose:
countPurpose:
Optional because it is not required by the current
hebo-gatewaybenchmark, but included for completeness.Where Semantics
The where model is intentionally small and generic.
Supported leaf operators in v1:
eq,negt,gte,lt,ltein,not_inSupported logical composition in v1:
andorNotes:
json_eqoperator in v1.Ordering and Pagination
sortBySupports sorting by one or more fields.
cursorThe cursor shape is intentionally generic:
This allows backends to implement cursor pagination according to their own storage model while still constraining cursor fields to keys of
T.Why This Shape
This interface is based on the combined lessons from:
hebo-gatewaystorage-layer discussion in issue#83The key decisions are:
Out of Scope
This spec does not define:
Current Recommendation
Use this interface as the v1 adapter contract.
Start with:
That pair is enough to validate whether the interface is genuinely portable.