Flutter数据库管理插件baserow的使用

发布于 1周前 作者 yuanlaile 来自 Flutter

Flutter数据库管理插件baserow的使用

特性

  • 📤 文件上传
    • 支持向Baserow上传文件
    • 支持带缩略图的图片上传
    • 提供详细的文件元数据
  • 🔐 身份验证支持
    • API令牌身份验证
    • JWT身份验证并支持刷新功能
  • 📚 数据库管理
    • 列出所有可访问的数据库
    • 查看数据库详细信息
  • 📋 表操作
    • 列出数据库中的表
    • 查看表结构和字段
    • 创建新表及初始数据
    • 创建和管理表字段
  • 📝 行操作
    • 列出表中的行
    • 创建新行
    • 更新现有行
    • 删除行
    • 批量创建/更新/删除多行
  • 🔄 通过WebSocket进行实时更新
    • 订阅表更改
    • 订阅工作区事件
    • 订阅应用事件
    • 订阅用户事件
    • 自动重新连接处理
  • 🛠️ 类型安全的数据模型
  • ⚡ 高效的HTTP连接管理
  • 🧪 综合测试工具

安装

在项目的 pubspec.yaml 中添加以下依赖:

dependencies:
  baserow: ^0.1.0

然后运行:

dart pub get

使用

身份验证

API Token身份验证
import 'package:baserow/baserow.dart';

// 创建一个带有API token的客户端实例
final client = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: 'YOUR_API_TOKEN',
  ),
);
JWT身份验证
import 'package:baserow/baserow.dart';

// 创建一个客户端实例
final client = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    authType: BaserowAuthType.jwt,
  ),
);

// 登录以获取JWT令牌
final authResponse = await client.login(
  'your.email@example.com',
  'your_password',
);
print('JWT Token: ${authResponse.token}');
print('Refresh Token: ${authResponse.refreshToken}');

// 使用JWT令牌创建一个新的客户端
final authenticatedClient = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: authResponse.token,
    authType: BaserowAuthType.jwt,
  ),
);

// 可以验证令牌是否仍然有效
final isValid = await authenticatedClient.verifyToken(authResponse.token);

// 当令牌过期时,可以刷新它
final newToken = await authenticatedClient.refreshToken(authResponse.refreshToken);

// 使用刷新后的令牌创建一个新的客户端
final refreshedClient = BaserowClient(
  config: BaserowConfig(
    baseUrl: 'https://api.baserow.io',
    token: newToken,
    authType: BaserowAuthType.jwt,
  ),
);

// 完成后,登出以使令牌失效
await authenticatedClient.logout();
// 登出后,客户端的令牌被清除,并停止刷新计时器

JWT身份验证流程提供了比API令牌更多的安全性特性,包括:

  • 令牌过期和刷新功能
  • 令牌验证
  • 包含在身份验证响应中的用户信息(authResponse.user

列出数据库和表

// 列出所有可访问的数据库
final databases = await client.listDatabases();
for (final db in databases) {
  print('Database: ${db.name} (ID: ${db.id})');

  // 列出数据库中的表
  final tables = await client.listTables(db.id);
  for (final table in tables) {
    print('Table: ${table.name} (ID: ${table.id})');
  }
}

文件上传

// 上传本地文件
final fileBytes = await File('image.png').readAsBytes();
final response = await client.uploadFile(fileBytes, 'image.png');

// 从URL上传文件
final urlResponse = await client.uploadFileViaUrl('https://example.com/image.png');

// 访问文件信息
print('File URL: ${response.url}');
print('File name: ${response.name}');
print('File size: ${response.size}');
print('MIME type: ${response.mimeType}');

// 对于图片,可以访问缩略图和尺寸
if (response.isImage) {
  print('Image width: ${response.imageWidth}');
  print('Image height: ${response.imageHeight}');

  // 访问缩略图
  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}');
  }
}

两种上传方法都返回一个包含以下信息的 FileUploadResponse

  • url: 上传文件的直接URL
  • name: 服务器上的文件名
  • size: 文件大小(字节)
  • mimeType: 文件的MIME类型
  • isImage: 文件是否为图像
  • imageWidthimageHeight: 图像文件的尺寸
  • thumbnails: 可用缩略图的映射及其URL和尺寸
  • uploadedAt: 文件上传的时间戳

数据库令牌

数据库令牌提供了一种以特定权限访问和管理表格数据的方式。每个令牌与一个工作空间关联,可以有不同的权限来创建、读取、更新和删除行。

// 列出所有数据库令牌
final tokens = await client.listDatabaseTokens();
for (final token in tokens) {
  print('Token: ${token.name}');
  print('Workspace: ${token.workspace}');
  print('Key: ${token.key}');

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

// 通过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}');
  }
}

// 删除一个数据库令牌
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}');
  }
}

// 检查数据库令牌是否有效
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}');
  }
}

权限可以是:

  • 布尔值(true/false)表示完全访问或无访问
  • 列表 [["database", id], ["table", id]] 表示对特定数据库或表的细粒度访问

例如:

// 全访问令牌
{
  "create": true,  // 可以在所有表中创建行
  "read": true,    // 可以从所有表中读取行
  "update": true,  // 可以更新所有表中的行
  "delete": true   // 可以从所有表中删除行
}

// 有限访问令牌
{
  "create": false,  // 不能创建行
  "read": [["database", 1], ["table", 10]],  // 只能从数据库1和表10读取
  "update": false,  // 不能更新行
  "delete": []     // 不能删除行
}

工作区

工作区是可以容纳多个应用程序(如数据库)的容器。多个用户可以访问同一个工作区,每个用户可以在工作区内拥有不同的权限。

// 列出所有工作区
final workspaces = await client.listWorkspaces();
for (final workspace in workspaces) {
  print('Workspace: ${workspace.name}');

  // 访问工作区详细信息
  print('ID: ${workspace.id}');
  print('Permissions: ${workspace.permissions}');
  print('Unread Notifications: ${workspace.unreadNotificationsCount}');
  print('AI Models Enabled: ${workspace.generativeAiModelsEnabled}');

  // 列出工作区用户
  for (final user in workspace.users) {
    print('User: ${user.name} (${user.email})');
    print('Role: ${user.permissions}');
    print('Created on: ${user.createdOn}');
  }
}

工作区列表提供了:

  • 基本工作区信息(ID、名称、权限)
  • 工作区用户的列表及其详细信息
  • 用户特定信息,如未读通知数量
  • 工作区设置,如启用的AI模型
  • 每个用户的工作区自定义排序(可通过 order_workspaces 端点配置)

视图操作

视图提供了不同的方式来显示和与表格数据交互。每个表格可以有多种视图(网格、画廊、表单、看板、日历、时间轴),每种视图有自己的设置。

// 创建一个新的网格视图
final gridView = await client.createView(
  tableId,
  name: "Main Grid",
  type: "grid",
  filterType: "AND",
  filtersDisabled: false,
);

// 创建一个公开的画廊视图
final galleryView = await client.createView(
  tableId,
  name: "Public Gallery",
  type: "gallery",
  public: true,
);

// 列出表格的所有视图
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}');
}

// 获取特定视图
final view = await client.getView(viewId);
print('View name: ${view.name}');
print('Filter type: ${view.filterType}');
print('Filters disabled: ${view.filtersDisabled}');

// 更新视图
final updatedView = await client.updateView(
  viewId,
  name: "Updated View",
  filterType: "OR",
  filtersDisabled: true,
);

// 删除视图
await client.deleteView(viewId);

每种视图类型都有其特定的功能:

  • 网格:传统的电子表格视图,带有行和列
  • 画廊:卡片式视图,适合视觉内容
  • 表单:可定制的表单用于数据输入
  • 看板:用于组织项目项的面板视图
  • 日历:基于日期的视图,用于时间数据
  • 时间轴:基于时间的视图,用于项目规划

视图可以:

  • 公开或私密(通过 public 参数控制)
  • 协作或个人(通过 ownershipType 设置)
  • 使用各种条件过滤(通过 filterTypefiltersDisabled 配置)

表操作

创建和确保表格

你可以以两种方式创建表格:

  1. 基本表格创建:
// 创建一个新的表格
final table = await client.createTable(
  databaseId,
  name: "Customers",
  data: [
    ["Name", "Email", "Status"],           // 字段名称
    ["John Doe", "john@example.com", "Active"],  // 初始数据
  ],
  firstRowHeader: true,  // 使用第一行为字段名称
);

// 创建一个不带初始数据的表格
final emptyTable = await client.createTable(
  databaseId,
  name: "Products",
);
  1. 使用 TableBuilder 进行声明式表格创建和更新:
// 定义具有字段和视图的表格结构
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"],
    ]),
);

// ensureTable 方法将执行以下操作:
// 1. 如果不存在,则创建表格
// 2. 如果存在且 updateIfExists 为 true(默认),则更新表格
// 3. 如果 updateIfExists 为 false,则返回现有表格而不做任何更改

// 你还可以使用它来确保不同环境之间一致的数据模型:
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"),
);

TableBuilder 提供了一个流式接口,用于定义:

  • 表格名称和结构
  • 字段及其类型和选项
  • 视图(网格、画廊、表单等)
  • 初始数据
  • 字段验证(必填字段等)
  • 字段描述和元数据

这特别适用于:

  • 在不同环境中设置一致的表格结构
  • 在版本控制中维护数据模型
  • 在测试中自动创建和更新表格
  • 确保所需字段和视图存在
管理字段
// 创建一个文本字段
final nameField = await client.createField(
  tableId,
  name: "Name",
  type: "text",
  options: {"text_default": "New Customer"},
);

// 创建一个数字字段
final priceField = await client.createField(
  tableId,
  name: "Price",
  type: "number",
  options: {
    "number_decimal_places": 2,
    "number_negative": true,
  },
);

// 列出表中的所有字段
final fields = await client.listFields(tableId);
for (final field in fields) {
  print('Field: ${field.name} (Type: ${field.type})');
}

// 更新一个字段
final updatedField = await client.updateField(
  fieldId,
  name: "Full Name",
  description: "Customer's full name",
);

// 删除一个字段
await client.deleteField(fieldId);

处理行

获取单行
// 通过ID获取单行
final row = await client.getRow(tableId, rowId);
print('Row ID: ${row.id}');
print('Field value: ${row.fields['field_1']}');

// 使用人类可读的字段名称获取单行
final row = await client.getRow(
  tableId,
  rowId,
  userFieldNames: true,
);
print('Name: ${row.fields['Name']}');
print('Email: ${row.fields['Email']}');

getRow 方法允许你:

  • 通过行ID获取特定行
  • 使用 userFieldNames: true 启用人类可读的字段名称
  • 通过 fields 属性访问所有字段值
  • 处理常见的错误,如不存在的行或权限问题
字段名称格式

Baserow 支持两种字段名称格式:

  • 默认格式:使用字段ID(例如 field_123
  • 用户友好格式:使用人类可读的字段名称(例如 NameEmail

你可以通过在相关操作中设置 userFieldNames: true 来启用用户友好的字段名称。

// 使用用户友好的字段名称列出表中的行
final rows = await client.listRows(
  tableId,
  options: ListRowsOptions(userFieldNames: true),
);

// 使用用户友好的字段名称创建新行
final newRow = await client.createRow(
  tableId,
  {
    'Name': 'John Doe',
    'Email': 'john@example.com',
  },
  userFieldNames: true,
);

// 使用用户友好的字段名称更新现有行
await client.updateRow(
  tableId,
  rowId,
  {
    'Name': 'Jane Doe',
    'Email': 'jane@example.com',
  },
  userFieldNames: true,
);

// 删除单行
await client.deleteRow(tableId, rowId);  // 带webhooks
await client.deleteRow(tableId, rowId, sendWebhookEvents: false);  // 不带webhooks

// 批量删除多行
await client.deleteRows(tableId, [123, 456]);  // 带webhooks
await client.deleteRows(tableId, [123, 456], sendWebhookEvents: false);  // 不带webhooks

// 将行移动到新位置
final movedRow = await client.moveRow(
  tableId,
  rowId,
  options: MoveRowOptions(
    beforeId: 456,  // 移动到此行之前
    userFieldNames: true,  // 使用人类可读的字段名称
    sendWebhookEvents: true,  // 移动后触发webhooks
  ),
);

// 将行移动到表末尾
final movedToEnd = await client.moveRow(
  tableId,
  rowId,
);
MoveRowOptions

用于自定义行移动操作的选项:

MoveRowOptions({
  bool userFieldNames = false,  // 使用人类可读的字段名称
  int? beforeId,               // 移动到此行之前(null表示移动到末尾)
  bool sendWebhookEvents = true, // 是否触发webhooks
})

移动操作允许你:

  • 使用 beforeId 将行移动到另一特定行之前
  • 通过省略 beforeId 将行移动到表末尾
  • 使用 sendWebhookEvents 控制webhooks触发
  • 使用 userFieldNames 在响应中使用人类可读的字段名称

清理

完成操作后,记得关闭客户端以释放资源:

client.close();

错误处理

该库会抛出 BaserowException 以处理API错误,其中包括:

  • 错误消息
  • HTTP状态码

示例错误处理:

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');
}

测试支持

SDK 提供了内置的测试工具,帮助你编写使用Baserow的应用程序的测试。这些工具使得模拟REST API调用和WebSocket实时事件变得容易。

安装

pubspec.yamldev_dependencies 中添加SDK:

dev_dependencies:
  baserow: ^0.1.0
  test: ^1.24.0

模拟REST API调用

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

void main() {
  test('fetching rows', () async {
    // 创建一个模拟客户端
    final mockClient = BaserowTestUtils.createMockClient();

    // 配置模拟响应
    when(mockClient.listRows(1)).thenAnswer((_) async => RowsResponse(
          count: 1,
          next: null,
          previous: null,
          results: [
            Row(id: 1, order: 1, fields: {'name': 'Test Row'}),
          ],
        ));

    // 使用模拟客户端
    final rows = await mockClient.listRows(1);
    expect(rows.results.first.fields['name'], equals('Test Row'));
  });
}

测试实时事件

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

void main() {
  test('receiving real-time updates', () async {
    // 创建一个模拟WebSocket
    final mockWebSocket = BaserowTestUtils.createMockWebSocket();
    await mockWebSocket.connect();

    // 订阅表事件
    final subscription = mockWebSocket.subscribeToTable(1);

    // 发送测试事件
    mockWebSocket.emitTableEvent(
      1,
      'row_created',
      {
        'row_id': 1,
        'values': {'name': 'New Row'},
      },
    );

    // 验证事件已被接收
    await expectLater(
      subscription,
      emits(isA<BaserowTableEvent>()
          .having((e) => e.type, 'type', 'row_created')
          .having((e) => e.tableId, 'tableId', 1)),
    );
  });
}

用户流

客户端提供了一个当前用户的流,你可以监听以跟踪认证状态变化:

// 获取用户流
Stream<User?> userStream = client.userStream;

// 监听用户变化
userStream.listen((user) {
  if (user != null) {
    print('User is logged in: ${user.name}');
    print('Email: ${user.email}');
  } else {
    print('User has logged out');
  }
});

用户流发出:

  • 当用户登录或其信息发生变化时,发出一个 User 对象
  • 当用户注销时,发出 null

这特别适用于:

  • 跟踪认证状态变化
  • 根据用户登录状态更新UI
  • 实时访问当前用户信息

测试错误处理

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

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

  // 模拟错误
  mockWebSocket.emitError(Exception('Test error'));

  // 验证错误已被处理
  await Future.delayed(Duration.zero);
  expect(errorReceived, isTrue);
});

更多关于Flutter数据库管理插件baserow的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter数据库管理插件baserow的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,Baserow 是一个强大的数据库管理工具,它允许你通过图形用户界面(GUI)来管理和操作数据库,同时它也提供了API接口,可以与你的Flutter应用进行集成。尽管Baserow本身不是一个Flutter插件,但你可以通过其提供的REST API来进行数据交互。

以下是一个基本的示例,展示如何在Flutter中使用HTTP请求与Baserow进行交互。我们将使用http包来发送请求,并假设你已经在Baserow中设置好了一个数据库表。

首先,确保在pubspec.yaml文件中添加http依赖:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3  # 请检查最新版本号

然后,运行flutter pub get来安装依赖。

接下来,我们编写Flutter代码来与Baserow进行交互。假设我们有一个名为items的表,并且我们想要执行基本的CRUD(创建、读取、更新、删除)操作。

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Baserow Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BaserowExample(),
    );
  }
}

class BaserowExample extends StatefulWidget {
  @override
  _BaserowExampleState createState() => _BaserowExampleState();
}

class _BaserowExampleState extends State<BaserowExample> {
  final String apiUrl = 'https://your-baserow-instance.com/api/v1/your-database/tables/items/';
  final String apiToken = 'your-api-token';

  Future<void> createItem() async {
    final response = await http.post(
      Uri.parse(apiUrl),
      headers: <String, String>{
        'Authorization': 'Token $apiToken',
        'Content-Type': 'application/json',
      },
      body: jsonEncode(<String, dynamic>{
        'name': 'New Item',
        'description': 'This is a new item',
      }),
    );

    if (response.statusCode == 201) {
      print('Item created successfully!');
    } else {
      throw Exception('Failed to create item');
    }
  }

  Future<void> readItems() async {
    final response = await http.get(
      Uri.parse(apiUrl),
      headers: <String, String>{
        'Authorization': 'Token $apiToken',
      },
    );

    if (response.statusCode == 200) {
      final items = jsonDecode(response.body) as List<dynamic>;
      print('Items: $items');
    } else {
      throw Exception('Failed to read items');
    }
  }

  Future<void> updateItem(int id, String newName) async {
    final url = '$apiUrl$id/';
    final response = await http.patch(
      Uri.parse(url),
      headers: <String, String>{
        'Authorization': 'Token $apiToken',
        'Content-Type': 'application/json',
      },
      body: jsonEncode(<String, dynamic>{
        'name': newName,
      }),
    );

    if (response.statusCode == 200) {
      print('Item updated successfully!');
    } else {
      throw Exception('Failed to update item');
    }
  }

  Future<void> deleteItem(int id) async {
    final url = '$apiUrl$id/';
    final response = await http.delete(
      Uri.parse(url),
      headers: <String, String>{
        'Authorization': 'Token $apiToken',
      },
    );

    if (response.statusCode == 204) {
      print('Item deleted successfully!');
    } else {
      throw Exception('Failed to delete item');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Baserow Example'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: createItem,
              child: Text('Create Item'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: readItems,
              child: Text('Read Items'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => updateItem(1, 'Updated Name'), // Update item with ID 1
              child: Text('Update Item'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => deleteItem(1), // Delete item with ID 1
              child: Text('Delete Item'),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,我们定义了四个函数createItemreadItemsupdateItemdeleteItem,分别用于创建、读取、更新和删除Baserow中的数据项。注意,你需要替换apiUrlapiToken为你的Baserow实例的实际URL和API令牌。

此外,这个示例使用了硬编码的ID(例如updateItem(1, 'Updated Name')deleteItem(1))来更新和删除特定的项。在实际应用中,你可能需要根据用户的选择或其他逻辑来动态确定这些ID。

这个示例提供了一个基本的框架,你可以根据需要扩展和修改它以适应你的具体需求。

回到顶部