baserow 3.2.2 copy "baserow: ^3.2.2" to clipboard
baserow: ^3.2.2 copied to clipboard

A Dart client library for managing Baserow databases and tables.

Baserow Dart Client #

A Dart client library for interacting with the Baserow API. This package provides a simple and intuitive way to work with Baserow databases, tables, and rows in your Dart and Flutter applications.

Features #

  • ๐Ÿ“ค File uploads
    • Upload files to Baserow
    • Support for images with thumbnails
    • Detailed file metadata
  • ๐Ÿ” Authentication support
    • API token authentication
    • JWT authentication with refresh capabilities
  • ๐Ÿ“š Database management
    • List all accessible databases
    • View database details
  • ๐Ÿ“‹ Table operations
    • List tables in a database
    • View table structure and fields
    • Create new tables with initial data
    • Create and manage table fields
  • ๐Ÿ“ Row operations
    • List rows in a table
    • Create new rows
    • Update existing rows
    • Delete rows
    • Batch operations for creating/updating/deleting multiple rows
  • ๐Ÿ”„ Real-time updates via WebSocket
    • Subscribe to table changes
    • Subscribe to workspace events
    • Subscribe to application events
    • Subscribe to user events
    • Automatic reconnection handling
  • ๐Ÿ› ๏ธ Type-safe data models for Baserow entities
  • โšก Efficient HTTP connection management
  • ๐Ÿงช Comprehensive testing utilities

Installation #

Add this package to your project's pubspec.yaml:

dependencies:
  baserow: ^0.1.0
copied to clipboard

Then run:

dart pub get
copied to clipboard

Usage #

Authentication #

API Token Authentication

import 'package:baserow/baserow.dart';

// Create a client instance with API token
final client = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: 'YOUR_API_TOKEN',
  ),
);
copied to clipboard

JWT Authentication

import 'package:baserow/baserow.dart';

// Create a client instance
final client = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    authType: BaserowAuthType.jwt,
  ),
);

// Login to obtain JWT tokens
final authResponse = await client.login(
  'your.email@example.com',
  'your_password',
);
print('JWT Token: ${authResponse.token}');
print('Refresh Token: ${authResponse.refreshToken}');

// Create a new client with the JWT token
final authenticatedClient = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: authResponse.token,
    authType: BaserowAuthType.jwt,
  ),
);

// You can verify if the token is still valid
final isValid = await authenticatedClient.verifyToken(authResponse.token);

// When the token expires, you can refresh it
final newToken = await authenticatedClient.refreshToken(authResponse.refreshToken);

// Create a new client with the refreshed token
final refreshedClient = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: newToken,
    authType: BaserowAuthType.jwt,
  ),
);

// When you're done, you can logout to invalidate the tokens
await authenticatedClient.logout();
// After logout, the client's tokens are cleared and the refresh timer is stopped
copied to clipboard

The JWT authentication flow provides more security features compared to API tokens, including:

  • Token expiration and refresh capabilities
  • Token verification
  • User information included in the auth response (authResponse.user)

Listing Databases and Tables #

// List all accessible databases
final databases = await client.listDatabases();
for (final db in databases) {
  print('Database: ${db.name} (ID: ${db.id})');

  // List tables in the database
  final tables = await client.listTables(db.id);
  for (final table in tables) {
    print('Table: ${table.name} (ID: ${table.id})');
  }
}
copied to clipboard

File Uploads #

// Upload a local file
final fileBytes = await File('image.png').readAsBytes();
final response = await client.uploadFile(fileBytes, 'image.png');

// Upload a file from a URL
final urlResponse = await client.uploadFileViaUrl('https://example.com/image.png');

// Access file information
print('File URL: ${response.url}');
print('File name: ${response.name}');
print('File size: ${response.size}');
print('MIME type: ${response.mimeType}');

// For images, you can access thumbnails and dimensions
if (response.isImage) {
  print('Image width: ${response.imageWidth}');
  print('Image height: ${response.imageHeight}');

  // Access thumbnails
  for (final entry in response.thumbnails.entries) {
    print('Thumbnail ${entry.key}:');
    print('  URL: ${entry.value.url}');
    print('  Width: ${entry.value.width}');
    print('  Height: ${entry.value.height}');
  }
}
copied to clipboard

Both upload methods return a FileUploadResponse that includes:

  • url: Direct URL to the uploaded file
  • name: The file name on the server
  • size: File size in bytes
  • mimeType: The file's MIME type
  • isImage: Whether the file is an image
  • imageWidth and imageHeight: Dimensions for image files
  • thumbnails: Map of available thumbnails with their URLs and dimensions
  • uploadedAt: Timestamp of when the file was uploaded

Database Tokens #

Database tokens provide a way to access and manage table data with specific permissions. Each token is associated with a workspace and can have different permissions for creating, reading, updating, and deleting rows.

// List all database tokens
final tokens = await client.listDatabaseTokens();
for (final token in tokens) {
  print('Token: ${token.name}');
  print('Workspace: ${token.workspace}');
  print('Key: ${token.key}');

  // Check permissions
  final perms = token.permissions;
  print('Can create: ${perms.create}');
  print('Can read: ${perms.read}');
  print('Can update: ${perms.update}');
  print('Can delete: ${perms.delete}');
}

// Get a specific database token by ID
try {
  final token = await client.getDatabaseToken(123);
  print('Token: ${token.name}');
  print('Workspace: ${token.workspace}');
  print('Key: ${token.key}');
} on BaserowException catch (e) {
  if (e.message == 'ERROR_TOKEN_DOES_NOT_EXIST') {
    print('Token not found');
  } else if (e.message == 'ERROR_USER_NOT_IN_GROUP') {
    print('User not authorized to access this token');
  } else {
    print('Error: ${e.message}');
  }
}

// Delete a database token
try {
  await client.deleteDatabaseToken(123);
  print('Token deleted successfully');
} on BaserowException catch (e) {
  if (e.message == 'ERROR_TOKEN_DOES_NOT_EXIST') {
    print('Token not found');
  } else if (e.message == 'ERROR_USER_NOT_IN_GROUP') {
    print('User not authorized to delete this token');
  } else {
    print('Error: ${e.message}');
  }
}

// Check if a database token is valid
try {
  await client.checkDatabaseToken();
  print('Token is valid');
} on BaserowException catch (e) {
  if (e.message == 'ERROR_TOKEN_DOES_NOT_EXIST') {
    print('Token is invalid');
  } else {
    print('Error: ${e.message}');
  }
}
copied to clipboard

Permissions can be either:

  • Boolean values (true/false) for full access or no access
  • Lists of [["database", id], ["table", id]] for granular access to specific databases or tables

For example:

// Full access token
{
  "create": true,  // Can create rows in all tables
  "read": true,    // Can read rows from all tables
  "update": true,  // Can update rows in all tables
  "delete": true   // Can delete rows from all tables
}

// Limited access token
{
  "create": false,  // Cannot create rows
  "read": [["database", 1], ["table", 10]],  // Can only read from database 1 and table 10
  "update": false,  // Cannot update rows
  "delete": []     // Cannot delete rows
}
copied to clipboard

Workspaces #

Workspaces are containers that can hold multiple applications like databases. Multiple users can have access to a workspace, and each user can have different permissions within the workspace.

// List all workspaces
final workspaces = await client.listWorkspaces();
for (final workspace in workspaces) {
  print('Workspace: ${workspace.name}');

  // Access workspace details
  print('ID: ${workspace.id}');
  print('Permissions: ${workspace.permissions}');
  print('Unread Notifications: ${workspace.unreadNotificationsCount}');
  print('AI Models Enabled: ${workspace.generativeAiModelsEnabled}');

  // List workspace users
  for (final user in workspace.users) {
    print('User: ${user.name} (${user.email})');
    print('Role: ${user.permissions}');
    print('Created on: ${user.createdOn}');
  }
}
copied to clipboard

The workspace listing provides:

  • Basic workspace information (ID, name, permissions)
  • List of workspace users with their details
  • User-specific information like unread notification count
  • Workspace settings like enabled AI models
  • Custom ordering of workspaces per user (configurable via the order_workspaces endpoint)

View Operations #

Views provide different ways to display and interact with table data. Each table can have multiple views (grid, gallery, form, kanban, calendar, timeline) with their own settings.

// Create a new grid view
final gridView = await client.createView(
  tableId,
  name: "Main Grid",
  type: "grid",
  filterType: "AND",
  filtersDisabled: false,
);

// Create a public gallery view
final galleryView = await client.createView(
  tableId,
  name: "Public Gallery",
  type: "gallery",
  public: true,
);

// List all views for a table
final views = await client.listViews(tableId);
for (final view in views) {
  print('View: ${view.name} (Type: ${view.type})');
  print('Public: ${view.public}');
  print('Slug: ${view.slug}');
}

// Get a specific view
final view = await client.getView(viewId);
print('View name: ${view.name}');
print('Filter type: ${view.filterType}');
print('Filters disabled: ${view.filtersDisabled}');

// Update a view
final updatedView = await client.updateView(
  viewId,
  name: "Updated View",
  filterType: "OR",
  filtersDisabled: true,
);

// Delete a view
await client.deleteView(viewId);
copied to clipboard

Each view type has its own specific features:

  • Grid: Traditional spreadsheet-like view with rows and columns
  • Gallery: Card-based view ideal for visual content
  • Form: Customizable form for data entry
  • Kanban: Board view for organizing items into columns
  • Calendar: Date-based view for temporal data
  • Timeline: Time-based view for project planning

Views can be:

  • Public or private (controlled via the public parameter)
  • Collaborative or personal (set via ownershipType)
  • Filtered using various conditions (configured with filterType and filtersDisabled)

Table Operations #

Creating and Ensuring Tables

You can create tables in two ways:

  1. Basic table creation:
// Create a new table
final table = await client.createTable(
  databaseId,
  name: "Customers",
  data: [
    ["Name", "Email", "Status"],           // Field names
    ["John Doe", "john@example.com", "Active"],  // Initial data
  ],
  firstRowHeader: true,  // Use first row as field names
);

// Create a table without initial data
final emptyTable = await client.createTable(
  databaseId,
  name: "Products",
);
copied to clipboard
  1. Using TableBuilder for declarative table creation and updates:
// Define a table structure with fields and views
final table = await client.ensureTable(
  databaseId,
  TableBuilder("Customers")
    ..withTextField("Name")
    ..withTextField("Email")
    ..withTextField("Status")
    ..withGridView("Main Grid")
    ..withData([
      ["John Doe", "john@example.com", "Active"],
      ["Jane Smith", "jane@example.com", "Pending"],
    ]),
);

// The ensureTable method will:
// 1. Create the table if it doesn't exist
// 2. Update the table if it exists and updateIfExists is true (default)
// 3. Return the existing table without changes if updateIfExists is false

// You can also use it to ensure a consistent data model across environments:
final customersTable = await client.ensureTable(
  databaseId,
  TableBuilder("Customers")
    ..withTextField("Name", required: true)
    ..withTextField("Email", required: true)
    ..withSelectField("Status", options: ["Active", "Pending", "Inactive"])
    ..withNumberField("Age", description: "Customer's age")
    ..withDateField("JoinDate")
    ..withGridView("All Customers")
    ..withGalleryView("Customer Cards")
    ..withFormView("New Customer"),
);
copied to clipboard

The TableBuilder provides a fluent interface for defining:

  • Table name and structure
  • Fields with their types and options
  • Views (Grid, Gallery, Form, etc.)
  • Initial data
  • Field validation (required fields, etc.)
  • Field descriptions and metadata

This is particularly useful for:

  • Setting up consistent table structures across different environments
  • Maintaining data models in version control
  • Automated table creation and updates in tests
  • Ensuring required fields and views exist

Managing Fields

// Create a text field
final nameField = await client.createField(
  tableId,
  name: "Name",
  type: "text",
  options: {"text_default": "New Customer"},
);

// Create a number field
final priceField = await client.createField(
  tableId,
  name: "Price",
  type: "number",
  options: {
    "number_decimal_places": 2,
    "number_negative": true,
  },
);

// List all fields in a table
final fields = await client.listFields(tableId);
for (final field in fields) {
  print('Field: ${field.name} (Type: ${field.type})');
}

// Update a field
final updatedField = await client.updateField(
  fieldId,
  name: "Full Name",
  description: "Customer's full name",
);

// Delete a field
await client.deleteField(fieldId);
copied to clipboard

Working with Rows #

Getting a Single Row

// Get a single row by ID
final row = await client.getRow(tableId, rowId);
print('Row ID: ${row.id}');
print('Field value: ${row.fields['field_1']}');

// Get a row using human-readable field names
final row = await client.getRow(
  tableId,
  rowId,
  userFieldNames: true,
);
print('Name: ${row.fields['Name']}');
print('Email: ${row.fields['Email']}');
copied to clipboard

The getRow method allows you to:

  • Fetch a specific row by its ID
  • Use human-readable field names with userFieldNames: true
  • Access all field values through the fields property
  • Handle common errors like non-existent rows or permission issues

Field Name Formats

Baserow supports two formats for field names:

  • Default format: Uses field IDs (e.g., field_123)
  • User-friendly format: Uses human-readable field names (e.g., Name, Email)

You can enable user-friendly field names by setting userFieldNames: true in the relevant operations.

// List rows from a table with user-friendly field names
final rows = await client.listRows(
  tableId,
  options: ListRowsOptions(userFieldNames: true),
);

// Create a new row with user-friendly field names
final newRow = await client.createRow(
  tableId,
  {
    'Name': 'John Doe',
    'Email': 'john@example.com',
  },
  userFieldNames: true,
);

// Update an existing row with user-friendly field names
await client.updateRow(
  tableId,
  rowId,
  {
    'Name': 'Jane Doe',
    'Email': 'jane@example.com',
  },
  userFieldNames: true,
);

// Delete a single row
await client.deleteRow(tableId, rowId);  // with webhooks
await client.deleteRow(tableId, rowId, sendWebhookEvents: false);  // without webhooks

// Delete multiple rows in batch
await client.deleteRows(tableId, [123, 456]);  // with webhooks
await client.deleteRows(tableId, [123, 456], sendWebhookEvents: false);  // without webhooks

// Move a row to a new position
final movedRow = await client.moveRow(
  tableId,
  rowId,
  options: MoveRowOptions(
    beforeId: 456,  // Move before this row
    userFieldNames: true,  // Use human-readable field names
    sendWebhookEvents: true,  // Trigger webhooks after move
  ),
);

// Move a row to the end of the table
final movedToEnd = await client.moveRow(
  tableId,
  rowId,
);
copied to clipboard

MoveRowOptions

Options for customizing row move operations:

MoveRowOptions({
  bool userFieldNames = false,  // Use human-readable field names
  int? beforeId,               // ID of row to move before (null = move to end)
  bool sendWebhookEvents = true, // Whether to trigger webhooks after move
})
copied to clipboard

The move operation allows you to:

  • Move a row before another specific row using beforeId
  • Move a row to the end of the table by omitting beforeId
  • Control webhook event triggering with sendWebhookEvents
  • Use human-readable field names in the response with userFieldNames

Cleanup #

Always close the client when you're done to free up resources:

client.close();
copied to clipboard

API Documentation #

BaserowConfig {#baserowconfig} #

Configuration class for the Baserow client.

BaserowConfig({
  required String baseUrl,  // The base URL of your Baserow instance
  String? token,           // Optional API token for authentication
})
copied to clipboard

BaserowClient {#baserowclient} #

Main client class for interacting with the Baserow API.

ListRowsOptions

Options for customizing row listing operations:

ListRowsOptions({
  int? page,                    // The page number to fetch (1-based)
  int? size,                    // The number of rows per page
  String? search,               // Search term to filter rows
  List<String>? orderBy,        // Fields to order by, with optional direction prefix (+ or -)
  String filterType = 'AND',    // Filter type - AND/OR for combining multiple filters
  List<RowFilter>? filters,     // JSON format filters
  Map<String, Map<String, dynamic>>? fieldFilters,  // Individual field filters (supports strings and integers)
  List<String>? include,        // Fields to include in the response
  List<String>? exclude,        // Fields to exclude from the response
  bool includeFieldMetadata,    // Whether to include field metadata
  int? viewId,                  // Optional view ID to scope the request
  bool userFieldNames = false,  // Use human-readable field names instead of field_123
  Map<String, List<String>>? linkRowJoins,  // Link row field joins for related table data
})
copied to clipboard
Ordering Rows

You can order rows by multiple fields using the orderBy parameter:

// Order by single field descending
options: ListRowsOptions(
  orderBy: ['-name'],
)

// Order by multiple fields
options: ListRowsOptions(
  orderBy: ['+first_name', '-last_name', 'age'],
)

// With special characters in field names
options: ListRowsOptions(
  orderBy: ['First, Name', 'Last "Name"'],  // Will be properly escaped
)
copied to clipboard
Filtering Rows

There are two ways to filter rows:

  1. Using the JSON format with filters:
options: ListRowsOptions(
  filterType: 'OR',  // Use OR to match any filter, AND to match all
  filters: [
    RowFilter(
      field: 'age',
      operator: FilterOperator.higherThan,
      value: 18,
    ),
    RowFilter(
      field: 'status',
      operator: FilterOperator.equal,
      value: 'active',
    ),
  ],
)
copied to clipboard
  1. Using individual field filters:
options: ListRowsOptions(
  fieldFilters: {
    'status': {'equal': 'active'},
    'age': {'greater_than': 18},      // Integer values are supported directly
    'rating': {'equal': 5},           // No need to convert to strings
  },
)
copied to clipboard
Including/Excluding Fields

Control which fields are returned in the response:

options: ListRowsOptions(
  include: ['name', 'email'],  // Only include these fields
  exclude: ['sensitive_data'], // Exclude these fields
)
copied to clipboard

Fetch related table data through link row fields:

options: ListRowsOptions(
  linkRowJoins: {
    'company': ['name', 'address'],  // Get company name and address
    'department': ['title'],         // Get department title
  },
)
copied to clipboard

Filter rows using a search term:

options: ListRowsOptions(
  search: 'search term',  // Will search across all searchable fields
)
copied to clipboard

Methods

  • Future<List<Database>> listDatabases()

    • Lists all databases accessible to the authenticated user
  • Future<List<Table>> listTables(int databaseId)

    • Lists all tables in a specific database
  • Future<List<Map<String, dynamic>>> listRows(int tableId)

    • Lists all rows in a specific table
  • Future<Map<String, dynamic>> createRow(int tableId, Map<String, dynamic> fields)

    • Creates a new row in a table
  • Future<Map<String, dynamic>> updateRow(int tableId, int rowId, Map<String, dynamic> fields)

    • Updates an existing row in a table
  • Future<void> deleteRow(int tableId, int rowId, {bool sendWebhookEvents = true})

    • Deletes a row from a table
    • Optional sendWebhookEvents parameter controls webhook triggering (defaults to true)
  • Future<void> deleteRows(int tableId, List<int> rowIds, {bool sendWebhookEvents = true})

    • Deletes multiple rows from a table in batch mode
    • Takes a list of row IDs to delete
    • Optional sendWebhookEvents parameter controls webhook triggering (defaults to true)

Models {#models} #

  • Database - Represents a Baserow database
  • Table - Represents a table within a database
  • Field - Represents a field within a table

Error Handling #

The library throws BaserowException for API errors, which includes:

  • Error message
  • HTTP status code

Example error handling:

try {
  final databases = await client.listDatabases();
} on BaserowException catch (e) {
  print('Baserow API error: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
  print('Unexpected error: $e');
}
copied to clipboard

Additional Information #

Testing Support #

The SDK provides built-in testing utilities to help you write tests for applications that use Baserow. These utilities make it easy to mock both REST API calls and WebSocket real-time events.

Installation #

Add the SDK to your dev_dependencies in pubspec.yaml:

dev_dependencies:
  baserow: ^0.1.0
  test: ^1.24.0
copied to clipboard

Mocking REST API Calls #

import 'package:baserow/baserow.dart';
import 'package:baserow/src/testing.dart';
import 'package:test/test.dart';

void main() {
  test('fetching rows', () async {
    // Create a mock client
    final mockClient = BaserowTestUtils.createMockClient();

    // Configure mock responses
    when(mockClient.listRows(1)).thenAnswer((_) async => RowsResponse(
          count: 1,
          next: null,
          previous: null,
          results: [
            Row(id: 1, order: 1, fields: {'name': 'Test Row'}),
          ],
        ));

    // Use the mock client
    final rows = await mockClient.listRows(1);
    expect(rows.results.first.fields['name'], equals('Test Row'));
  });
}
copied to clipboard

Testing Real-time Events {#testing-real-time-events} #

import 'package:baserow/baserow.dart';
import 'package:baserow/src/testing.dart';
import 'package:test/test.dart';

void main() {
  test('receiving real-time updates', () async {
    // Create a mock WebSocket
    final mockWebSocket = BaserowTestUtils.createMockWebSocket();
    await mockWebSocket.connect();

    // Subscribe to table events
    final subscription = mockWebSocket.subscribeToTable(1);

    // Emit a test event
    mockWebSocket.emitTableEvent(
      1,
      'row_created',
      {
        'row_id': 1,
        'values': {'name': 'New Row'},
      },
    );

    // Verify the event was received
    await expectLater(
      subscription,
      emits(isA<BaserowTableEvent>()
          .having((e) => e.type, 'type', 'row_created')
          .having((e) => e.tableId, 'tableId', 1)),
    );
  });
}
copied to clipboard

User Stream {#user-stream} #

The client provides a stream of the current user that you can listen to for authentication state changes:

// Get access to the user stream
Stream<User?> userStream = client.userStream;

// Listen for user changes
userStream.listen((user) {
  if (user != null) {
    print('User is logged in: ${user.name}');
    print('Email: ${user.email}');
  } else {
    print('User has logged out');
  }
});
copied to clipboard

The user stream emits:

  • A User object when the user logs in or when their information changes
  • null when the user logs out

This is particularly useful for:

  • Tracking authentication state changes
  • Updating UI based on user login status
  • Accessing current user information in real-time

Testing Error Handling {#testing-error-handling} #

test('handling WebSocket errors', () async {
  final mockWebSocket = BaserowTestUtils.createMockWebSocket();
  await mockWebSocket.connect();

  var errorReceived = false;
  mockWebSocket.onError = (error) {
    errorReceived = true;
  };

  // Simulate an error
  mockWebSocket.emitError(Exception('Test error'));

  // Verify error was handled
  await Future.delayed(Duration.zero);
  expect(errorReceived, isTrue);
});
copied to clipboard

For more examples, check out the testing examples in the repository.

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

1
likes
160
points
115
downloads

Publisher

unverified uploader

Weekly Downloads

2024.09.26 - 2025.04.10

A Dart client library for managing Baserow databases and tables.

Repository (GitHub)

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

http, json_annotation, jwt_decoder, web_socket_channel

More

Packages that depend on baserow