Flutter数据库管理插件baserow的使用
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
: 上传文件的直接URLname
: 服务器上的文件名size
: 文件大小(字节)mimeType
: 文件的MIME类型isImage
: 文件是否为图像imageWidth
和imageHeight
: 图像文件的尺寸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
设置) - 使用各种条件过滤(通过
filterType
和filtersDisabled
配置)
表操作
创建和确保表格
你可以以两种方式创建表格:
- 基本表格创建:
// 创建一个新的表格
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",
);
- 使用
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
) - 用户友好格式:使用人类可读的字段名称(例如
Name
,Email
)
你可以通过在相关操作中设置 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.yaml
的 dev_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
更多关于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'),
),
],
),
),
);
}
}
在这个示例中,我们定义了四个函数createItem
、readItems
、updateItem
和deleteItem
,分别用于创建、读取、更新和删除Baserow中的数据项。注意,你需要替换apiUrl
和apiToken
为你的Baserow实例的实际URL和API令牌。
此外,这个示例使用了硬编码的ID(例如updateItem(1, 'Updated Name')
和deleteItem(1)
)来更新和删除特定的项。在实际应用中,你可能需要根据用户的选择或其他逻辑来动态确定这些ID。
这个示例提供了一个基本的框架,你可以根据需要扩展和修改它以适应你的具体需求。