mortar_client 0.2.0 copy "mortar_client: ^0.2.0" to clipboard
mortar_client: ^0.2.0 copied to clipboard

Dart and Flutter client for Mortar, a mainland-China-friendly Backend-as-a-Service for mobile and web apps.

mortar_client (Dart / Flutter) #

Dart client for Mortar, a mainland-China-friendly Backend-as-a-Service.

Works in pure Dart, Flutter (iOS / Android / web / macOS / Windows / Linux), and Dart server. Anywhere package:http runs.

Module coverage #

The project-scope MortarClient exposes only the app-bundle (public-key) data plane: auth / db (from) / storage / realtime / compute.invoke. Provisioning/operator surfaces (tables-admin, compute.deploy/delete, cron, queue, comms, usage) are NOT on it — provisioning is mortar.config.json (server-side) and operator reads live on AccountClient (usage / crons / logs), reached with the account JWT.

AccountClient also covers admin (API keys / topup as flat methods: createKey / listKeys / revokeKey / topup).

Still TypeScript-only: cron/queue/comms/customDomains runtime management — call the HTTP API directly until the Dart port lands. Trusted server code can enqueue a PostgreSQL-backed job inside DatabaseTransactions, as shown below. Schemas: openapi.yaml.

Install #

# pubspec.yaml
dependencies:
  mortar_client: ^0.1.0

Or run dart pub add mortar_client.

Project-scope client #

import 'package:mortar_client/mortar_client.dart';

final mortar = MortarClient(ClientOptions(
  url: 'https://project-uuid.api.mortar.appunvs.com',
  apiKey: 'mtr_live_xxx',
));

// Database — Supabase-faithful chainable query builder.  Each call is
// awaitable, returns the data directly, and THROWS `MortarApiError` on error
// (the supabase-dart idiom — no `{data, error}` envelope).  Rows are FLAT
// (your fields plus id / created_at / updated_at at the top level).
final todos = await mortar
    .from('todos')
    .select()
    .eq('done', false)
    .order('created_at', ascending: false)
    .limit(50);
final row = await mortar.from('todos').insert({'title': 'buy milk'});
// Batch insert/upsert and filtered update/delete each execute atomically in
// one server-side PostgreSQL transaction:
await mortar.from('todos').update({'title': 'buy milk + eggs'}).eq('id', row['id']);
await mortar.from('todos').delete().eq('id', row['id']);
final one = await mortar.from('todos').select().eq('id', row['id']).single();
// Back-compat by-id forms also still work:
//   mortar.from('todos').getById(id) / updateById(id, data) / deleteById(id)

// End-user auth — Supabase surface (signInWithPassword / signInWithOtp /
// verifyOtp / getUser / getSession / refreshSession / signOut /
// onAuthStateChange / resetPasswordForEmail), all throw + return data directly.
final res = await mortar.auth.signInWithPassword(email: 'alice@example.com', password: '...');
final userScoped = mortar.withUserToken(res.accessToken); // see KNOWN DIFFERENCE below
mortar.auth.onAuthStateChange((event, session) => print('auth: $event'));
await mortar.auth.signInWithOtp(email: 'alice@example.com');     // send OTP
// final s = await mortar.auth.verifyOtp(email: 'alice@example.com', token: '123456');
final me = await mortar.auth.getUser();          // GET /auth/me
final current = mortar.auth.getSession();         // in-memory session or null
await mortar.auth.resetPasswordForEmail('alice@example.com');
await mortar.auth.signOut();

// Storage — supabase-dart `storage.from(bucket)` handle
await userScoped.storage
    .from('avatars')
    .upload('alice.jpg', await imageFile.readAsBytes(), contentType: 'image/jpeg');
final bytes = await userScoped.storage.from('avatars').download('alice.jpg');
final url = await userScoped.storage.from('avatars').createSignedUrl('alice.jpg', 3600);

// For media lists, lazy-load visible items and reuse each signed URL until it
// nears expiry. Signing is outside the project API-concurrency budget and is
// not itself an object-egress charge; the URL downloads directly from storage.

// Realtime — supabase `mortar.channel(name)` + `channel.on('postgres_changes', ...)`
final room = mortar.channel('todos-room')
  ..on('postgres_changes', {'event': '*', 'table': 'todos'},
      (p) => print('${p.eventType} ${p.newRecord}'));
room.subscribe();
// later: mortar.removeChannel(room);

// Realtime — low-level SSE subscription to row changes
final sub = mortar.realtime.subscribe(
  'todos',
  (ev) => print('${ev.op} ${ev.id}'),
  onError: (e) => print('realtime err: $e'),
);
// later: sub.unsubscribe();

// Compute — invoke a deployed function
final res = await mortar.compute.invoke(name: 'hello');
print(res.body);

Trusted server code using an app/admin key can combine cross-table row writes and PostgreSQL queue enqueue in one ACID transaction:

final transactions = DatabaseTransactions(ClientOptions(
  url: 'https://project-uuid.api.mortar.appunvs.com',
  apiKey: Platform.environment['MORTAR_APP_KEY']!,
));

await transactions.execute([
  DatabaseTransactionOperation.insert('orders', {'status': 'pending'}),
  DatabaseTransactionOperation.update('inventory', productId, {'stock': 9}),
  DatabaseTransactionOperation.enqueue(
    'send-receipt',
    payload: {'order_id': orderId},
  ),
]);

This boundary includes only the same PostgreSQL database. Object storage, external HTTP calls, email delivery, and deployed compute are not part of the transaction; model those as durable jobs with idempotent workers.

KNOWN DIFFERENCE vs the TS SDK. Dart auth tracks an in-memory current session (so getSession / onAuthStateChange work) but does not persist it to disk, auto-refresh it, or auto-attach the user token to subsequent requests. After signing in, attach the token yourself: final scoped = mortar.withUserToken(res.accessToken);.

Account-scope client #

import 'package:mortar_client/mortar_client.dart';

final account = AccountClient(url: 'https://api.mortar.appunvs.com');
await account.signIn(email: 'me@example.com', password: 'hunter2');

final projects = await account.listProjects();
final created = await account.createProject(name: 'staging', tier: 'mini');
await account.updateProject(created.id, tier: 'small');

// Operator reads (control plane) — usage / crons / function logs
final usage = await account.usage(created.id);
final crons = await account.crons(created.id);
final logs = await account.logs(created.id, 'hello', limit: 100);

await account.deleteProject(created.id);

Errors #

Every method throws MortarApiError on non-2xx.

try {
  await mortar.from('todos').insert({'title': 'x'});
} on MortarApiError catch (e) {
  // Stable fields: code, category, requestId, retryable, retryAfterMs.
  if (e.category == 'quota') {
    // top up, change plan, or wait
  } else {
    // map the category to the app's shared recovery UI
  }
}

compute.invoke returns a tenant Function's 4xx/5xx as http.Response; only a Mortar platform failure throws MortarApiError.

Run tests #

dart pub get
dart test

License #

Apache License 2.0.

0
likes
160
points
104
downloads

Documentation

Documentation
API reference

Publisher

verified publisherappunvs.com

Weekly Downloads

Dart and Flutter client for Mortar, a mainland-China-friendly Backend-as-a-Service for mobile and web apps.

Homepage
Repository (GitHub)
View/report issues

Topics

#baas #backend #database #realtime #storage

License

Apache-2.0 (license)

Dependencies

http

More

Packages that depend on mortar_client