Skip to main content
The data layer is Xolo’s persistence backbone. It owns everything below the domain boundary: the SQLite schema declared as Drift Table classes, the AppDatabase that stitches them together, focused query mixins for each concern, and the DriftXoloRepository that implements the abstract XoloRepository contract and translates raw Drift rows into domain entities. Nothing above this layer ever imports a Drift type directly — the presentation layer always talks to XoloRepository, never to AppDatabase.

Database Tables

AppDatabase registers eight Drift tables declared in tables.dart. Each table is a plain Dart class that extends Table; Drift’s code generator derives the companion and typed-row classes from it.
Represents both top-level workspace roots and nested folder nodes. The self-referential parentId FK creates the tree structure; when parentId is null, the row is a root collection (exposed by the CollectionEntity.isRoot helper).
Stores every reusable API request. A request can belong to a Collections row (via collectionId) or be unclassified when that FK is null. Soft-deletion is modelled with isDeleted rather than a physical row delete, preserving history references.
Automatic execution log. Every request fired from the UI appends a row here. workspaceId links the entry to the active collection workspace; savedRequestId optionally links it back to the originating SavedRequests row.
Named variable scopes (e.g. Dev, Staging, Production) that belong to a specific workspace collection. Setting isActive to true marks the environment currently selected for variable resolution in that workspace.
Stores key-value pairs scoped either to a specific environment (scope = 'env', environmentId != null) or to a workspace globally (scope = 'global', collectionId != null, environmentId == null). When both FKs are null the variable is user-global.
Simple key-value configuration store used for app-wide preferences (e.g. active workspace, last-run collection ID). The key column is the primary key, so upserts are idiomatic.
Persists the header record for every collection run execution. Step counts are updated atomically when finishCollectionRun is called.
One row per request step within a CollectionRuns run. assertionResultsJson serialises the full List<AssertionResultEntity> so the UI can replay every pass/fail detail.

DriftXoloRepository

DriftXoloRepository is the single concrete implementation of XoloRepository. It holds a private AppDatabase reference injected via its constructor and delegates every method to the appropriate query helper.
To keep files manageable, AppDatabase itself is split into focused part files — one per concern:

collection_queries.dart

CRUD, move, path traversal, and auth-data helpers for Collections and SavedRequests.

environment_queries.dart

Environment lifecycle and variable upsert/resolve logic.

history_queries.dart

watchRecentHistory, addHistoryItem, clear and single-entry helpers.

run_queries.dart

createCollectionRun, finishCollectionRun, insertRunStepResultRow, and watchers.

settings_queries.dart

setSetting, getSetting, watchSetting backed by AppSettings.
DriftXoloRepository acts as a thin adapter: it calls the database method and pipes the result through a mapper function, never leaking Drift row types upstream.

Entity Mappers

entity_mappers.dart defines Dart extension methods on every Drift-generated row type. Each extension adds a toEntity() method that constructs the corresponding domain class, keeping all field mapping in one place.
Top-level convenience functions (mapCollections, mapSavedRequests, mapHistoryEntries, mapEnvironments, mapEnvVariables, mapCollectionRuns, mapRunStepResults) accept a List of Drift rows and return an immutable List of entities for use in .map(...) stream transformers inside DriftXoloRepository. The presentation layer always works with domain entities and never imports anything from package:drift or package:xolo/data.

Reactive Streams

Drift exposes a Stream<T> for every watch* query. These streams re-emit automatically whenever any row in the watched table changes — no manual invalidation required. DriftXoloRepository wraps each stream with .map(mapper) to convert rows into entities before they reach Riverpod. Riverpod StreamProviders subscribe to these streams and expose them to the widget tree:
You can follow the same pattern for any new watch* method:
Because StreamProvider.autoDispose is used, the Drift query subscription is cancelled automatically when the last listener detaches, preventing background database activity when a screen is not visible.

Database Migrations

AppDatabase tracks schema evolution through an integer schemaVersion. When a new table or column is required, bump the version and add a guarded migration block inside onUpgrade. The current version history shows how Xolo has grown:
1

v1 — Initial schema

Core tables: SavedRequests, HistoryEntries, Collections, Environments, EnvVariables, AppSettings.
2

v2 — Auth columns

Added authType / authData to SavedRequests and HistoryEntries.
3

v3 — Body schema

Added schemaJson to SavedRequests for OpenAPI-driven body generation.
4

v4 — Collection auth

Added authType / authData to Collections for auth inheritance.
5

v5 — Post-request scripts

Added scriptsJson to SavedRequests for response-chaining rules.
6

v6 — Pre-request scripts

Added preScriptsJson to SavedRequests.
7

v7 — Template URL history

Added originalUrl to HistoryEntries to preserve the pre-substitution URL template.
8

v8 — Collection runs

Added assertionsJson to SavedRequests. Created CollectionRuns and RunStepResults tables.
To add a new migration, open database.dart and add a guarded block:
Never access AppDatabase directly from presentation code. All data access must go through xoloRepositoryProvider. This keeps the presentation layer decoupled from Drift, makes testing trivial (swap the provider with a mock), and ensures every query is mediated by the mapper layer.
After modifying tables.dart — adding columns, tables, or changing types — you must regenerate the Drift-produced code:
Failing to do so will cause compile errors because database.g.dart will be out of sync with the table declarations.