Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/JonathanHerSa/xolo-api-hub/llms.txt

Use this file to discover all available pages before exploring further.

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).
ColumnTypeNotes
idintAuto-increment PK
nametext1–100 chars, required
descriptiontext?Nullable free-text
parentIdint?Self-FK → Collections.id
authTypetext?'bearer', 'basic', 'inherit', etc.
authDatatext?JSON string with credentials
createdAtdateTimeDefaults to currentDateAndTime
class Collections extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text().withLength(min: 1, max: 100)();
  TextColumn get description => text().nullable()();
  IntColumn get parentId => integer().nullable().references(Collections, #id)();
  TextColumn get authType => text().nullable()();
  TextColumn get authData => text().nullable()();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
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.
ColumnTypeNotes
idintAuto-increment PK
nametext1–100 chars
methodtextGET, POST, etc.
urltextFull or template URL
headersJsontext?JSON map of headers
paramsJsontext?JSON map of query params
bodytext?Raw request body
authTypetext?Per-request auth override
authDatatext?JSON credentials
schemaJsontext?Resolved/dereferenced OpenAPI body schema
preScriptsJsontext?Variable extraction rules run before the request
scriptsJsontext?Post-request chaining rules (JSONPath → variable)
assertionsJsontext?Serialised List<AssertionRuleEntity>
collectionIdint?FK → Collections.id
createdAtdateTimeDefaults to currentDateAndTime
updatedAtdateTimeDefaults to currentDateAndTime
isDeletedboolSoft-delete flag, defaults false
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.
ColumnTypeNotes
idintAuto-increment PK
savedRequestIdint?FK → SavedRequests.id (nullable)
workspaceIdint?FK → Collections.id
methodtextHTTP verb
urltextResolved (absolute) URL
originalUrltext?Template URL before variable substitution
headersJsontext?Snapshot of sent headers
paramsJsontext?Snapshot of query params
bodytext?Sent body
authTypetext?Auth snapshot for reproducibility
authDatatext?Credential snapshot
statusCodeint?HTTP response code
responseBodytext?Full response body
durationMsint?Round-trip latency
executedAtdateTimeDefaults to currentDateAndTime
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.
ColumnTypeNotes
idintAuto-increment PK
nametext1–50 chars
collectionIdint?FK → Collections.id (workspace owner)
isActiveboolfalse by default
createdAtdateTimeDefaults to currentDateAndTime
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.
ColumnTypeNotes
idintAuto-increment PK
keytext1–100 chars
valuetextVariable value
environmentIdint?FK → Environments.id
collectionIdint?FK → Collections.id (workspace scope)
scopetext'global' or 'env', defaults 'global'
createdAtdateTimeDefaults to currentDateAndTime
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.
ColumnTypeNotes
keytextPK, 1–50 chars
valuetextSetting value
Persists the header record for every collection run execution. Step counts are updated atomically when finishCollectionRun is called.
ColumnTypeNotes
idintAuto-increment PK
collectionIdintFK → Collections.id
workspaceIdint?FK → Collections.id
environmentIdint?FK → Environments.id
statustext'running', 'completed', 'failed', 'cancelled'
totalStepsintPlan size at run creation
passedStepsintSteps whose assertions all passed
failedStepsintSteps with at least one failed assertion
skippedStepsintSteps skipped by RunOptions
stopOnFailureboolPropagated from RunOptions
runOptionsJsontext?Serialised RunOptions for replay
variablesSnapshotJsontext?Variable state at run completion
startedAtdateTimeDefaults to currentDateAndTime
finishedAtdateTime?Null while run is in progress
One row per request step within a CollectionRuns run. assertionResultsJson serialises the full List<AssertionResultEntity> so the UI can replay every pass/fail detail.
ColumnTypeNotes
idintAuto-increment PK
runIdintFK → CollectionRuns.id
savedRequestIdint?FK → SavedRequests.id
stepIndexintZero-based position in plan
nametextRequest name snapshot
methodtextHTTP verb
urltextResolved URL at execution time
stepStatustext'passed', 'failed', 'skipped', 'error'
statusCodeint?HTTP response code
durationMsint?Round-trip latency
passedboolComposite pass/fail flag
assertionResultsJsontext?Serialised List<AssertionResultEntity>
errorMessagetext?Network or evaluation error text
responseBodySnippettext?First 500 chars of the response body

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.
class DriftXoloRepository implements XoloRepository {
  DriftXoloRepository(this._db);
  final AppDatabase _db;
}
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.
@override
Stream<List<CollectionEntity>> watchAllCollections() =>
    _db.watchAllCollections().map(mapCollections);

@override
Future<CollectionEntity?> findCollectionByName(
  String name,
  int? parentId,
) async {
  final row = await _db.findCollectionByName(name, parentId);
  return row?.toEntity();
}

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.
extension CollectionEntityMapper on Collection {
  CollectionEntity toEntity() => CollectionEntity(
    id: id,
    name: name,
    description: description,
    parentId: parentId,
    authType: authType,
    authData: authData,
    createdAt: createdAt,
  );
}

extension RunStepResultEntityMapper on RunStepResult {
  RunStepResultEntity toEntity() {
    // Deserialises assertionResultsJson → List<AssertionResultEntity>
    final assertions = <AssertionResultEntity>[];
    if (assertionResultsJson != null && assertionResultsJson!.isNotEmpty) {
      try {
        final list = jsonDecode(assertionResultsJson!) as List<dynamic>;
        for (final item in list) {
          assertions.add(
            AssertionResultEntity.fromJson(item as Map<String, dynamic>),
          );
        }
      } catch (_) {}
    }
    return RunStepResultEntity(
      stepIndex: stepIndex,
      name: name,
      method: method,
      url: url,
      status: RunStepStatus.values.firstWhere(
        (s) => s.name == stepStatus,
        orElse: () => RunStepStatus.error,
      ),
      // ...
    );
  }
}
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:
// From database_providers.dart
final xoloRepositoryProvider = Provider<XoloRepository>((ref) {
  return DriftXoloRepository(ref.watch(databaseProvider));
});

final savedRequestsStreamProvider =
    StreamProvider.autoDispose<List<SavedRequestEntity>>((ref) {
      final repo = ref.watch(xoloRepositoryProvider);
      return repo.watchSavedRequests();
    });
You can follow the same pattern for any new watch* method:
final collectionsProvider = StreamProvider<List<CollectionEntity>>((ref) {
  return ref.watch(xoloRepositoryProvider).watchAllCollections();
});
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:
@override
int get schemaVersion => 9; // bump this

@override
MigrationStrategy get migration {
  return MigrationStrategy(
    onUpgrade: (Migrator m, int from, int to) async {
      // ... existing guards ...
      if (from < 9) {
        await m.addColumn(savedRequests, savedRequests.myNewColumn);
      }
    },
  );
}
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:
dart run build_runner build --delete-conflicting-outputs
Failing to do so will cause compile errors because database.g.dart will be out of sync with the table declarations.

Build docs developers (and LLMs) love