@aiao/rxdb-adapter-supabase 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +280 -58
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,91 +1,313 @@
1
- # rxdb-adapter-supabase
1
+ # @aiao/rxdb-adapter-supabase
2
2
 
3
- Supabase database adapter for AIAO RxDB framework.
3
+ RxDB Supabase 适配器 - 基于 PostgreSQL 的远程同步实现。
4
4
 
5
- ## Features
5
+ ## 特性
6
6
 
7
- - ✅ Full CRUD operations (Create, Read, Update, Delete)
8
- - ✅ Complex query conditions (`=`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `between`, `startsWith`, etc.)
9
- - ✅ Relationship queries (ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY)
10
- - ✅ EXISTS / NOT EXISTS operators for checking related records
11
- - ✅ COUNT queries with EXISTS support
12
- - ✅ Ordering and pagination
13
- - ✅ Real-time subscriptions via Supabase Realtime
7
+ - ✅ **完整的 CRUD 操作** - 通过 Repository 模式封装 Supabase API
8
+ - ✅ **批量操作** - `saveMany` / `removeMany` / `mutations` 高性能批处理
9
+ - ✅ **事务支持** - 基于 PostgreSQL RPC 的原子性操作
10
+ - ✅ **实时订阅** - Supabase Realtime 自动推送远程变更
11
+ - ✅ **变更追踪** - 自动记录所有数据变更到 `RxDBChange`
12
+ - ✅ **双向同步** - Push/Pull 机制确保本地与远程数据一致
13
+ - ✅ **树形结构** - `SupabaseTreeRepository` 支持树形数据操作
14
14
 
15
- ## Query Operators
15
+ ## 安装
16
16
 
17
- ### Comparison Operators
18
-
19
- - `=`, `!=` - Equal / Not equal
20
- - `>`, `>=`, `<`, `<=` - Greater / Less than
21
- - `in`, `notIn` - Value in / not in array
22
- - `between` - Value between range
23
- - `startsWith`, `endsWith`, `contains` - String pattern matching
24
-
25
- ### Relationship Operators
17
+ ```bash
18
+ pnpm add @aiao/rxdb-adapter-supabase @supabase/supabase-js
19
+ ```
26
20
 
27
- - `exists` - Check if related record exists
28
- - `notExists` - Check if related record does not exist
21
+ ## 快速开始
29
22
 
30
- ## EXISTS Operator Examples
23
+ ### 1. 配置 RxDB
31
24
 
32
25
  ```typescript
33
- // Find users who have orders
34
- // 注意:使用实体定义中的关系名 'orders',而不是表名 'Order'
35
- const usersWithOrders = await userRepo.find({
36
- where: {
37
- combinator: 'and',
38
- rules: [{ field: 'orders', operator: 'exists' }]
39
- }
40
- });
26
+ import { RxDB } from '@aiao/rxdb';
27
+ import { RxDBAdapterSupabase } from '@aiao/rxdb-adapter-supabase';
28
+ import { RxDBAdapterSqlite } from '@aiao/rxdb-adapter-sqlite';
41
29
 
42
- // Find users who don't have id cards
43
- // 使用关系名 'idCard',而不是表名 'IdCard'
44
- const usersWithoutIdCard = await userRepo.find({
45
- where: {
46
- combinator: 'and',
47
- rules: [{ field: 'idCard', operator: 'notExists' }]
30
+ const rxdb = new RxDB({
31
+ dbName: 'my-app',
32
+ context: { userId: 'user-123' },
33
+ entities: [Todo, User],
34
+ sync: {
35
+ local: { adapter: 'sqlite' },
36
+ remote: { adapter: 'supabase' },
37
+ type: SyncType.PullPush
48
38
  }
49
39
  });
50
40
 
51
- // Count users with orders
52
- const count = await userRepo.count({
41
+ // 注册适配器
42
+ rxdb.adapter('sqlite', db => new RxDBAdapterSqlite(db, { vfs: 'MemoryAsyncVFS' }));
43
+
44
+ rxdb.adapter(
45
+ 'supabase',
46
+ db =>
47
+ new RxDBAdapterSupabase(db, {
48
+ supabaseUrl: 'https://your-project.supabase.co',
49
+ supabaseKey: 'your-anon-key'
50
+ })
51
+ );
52
+
53
+ // 连接
54
+ await rxdb.connect('sqlite');
55
+ await rxdb.connect('supabase');
56
+ ```
57
+
58
+ ### 2. 数据操作
59
+
60
+ ```typescript
61
+ // 创建
62
+ const todo = rxdb.repository(Todo).create();
63
+ todo.title = 'Learn RxDB';
64
+ await todo.save();
65
+
66
+ // 查询
67
+ const todos = await rxdb.repository(Todo).find({
53
68
  where: {
54
69
  combinator: 'and',
55
- rules: [{ field: 'orders', operator: 'exists' }]
70
+ rules: [{ field: 'completed', operator: '=', value: false }]
56
71
  }
57
72
  });
73
+
74
+ // 同步
75
+ await rxdb.versionManager.push(); // 推送本地变更到远程
76
+ await rxdb.versionManager.pull(); // 拉取远程变更到本地
77
+ await rxdb.versionManager.sync(); // 双向同步 (pull + push)
58
78
  ```
59
79
 
60
- **重要**:`field` 参数必须使用实体定义中的**关系名**(如 `'idCard'`, `'orders'`),而不是 Supabase 表名(如 `'IdCard'`, `'Order'`)。
80
+ ## 测试
61
81
 
62
- ## Known Limitations
82
+ ### 前置条件
63
83
 
64
- ### Bidirectional ONE_TO_ONE Relationships
84
+ 测试依赖本地 Supabase 环境,确保:
65
85
 
66
- When a ONE_TO_ONE relationship has foreign keys on both sides (e.g., `User.idCardId` → `IdCard` and `IdCard.ownerId` → `User`), Supabase cannot automatically determine which foreign key to use for EXISTS queries.
86
+ 1. **Docker 已安装并运行**
87
+ 2. **启动 Supabase 测试容器**:
67
88
 
68
- **Workaround**: Redesign schema to have foreign key on only one side, or use the reverse relationship query.
89
+ ```bash
90
+ cd docker
91
+ bash start.sh
92
+ ```
93
+
94
+ 这会启动并初始化:
95
+ - PostgreSQL (端口 5432)
96
+ - Kong API Gateway (端口 8000)
97
+ - 创建必要的表和函数
98
+
99
+ 3. **环境变量配置**:
100
+ 测试自动使用以下配置:
101
+ - `VITE_SUPABASE_URL=http://localhost:8000`
102
+ - `VITE_SUPABASE_KEY=<anon-key>` (见 `docker/.env`)
103
+
104
+ ### 运行测试
105
+
106
+ ```bash
107
+ # 确保 Supabase 已启动
108
+ cd docker && bash start.sh
109
+
110
+ # 运行测试
111
+ nx test rxdb-adapter-supabase
112
+
113
+ # 带覆盖率
114
+ nx test rxdb-adapter-supabase --coverage
115
+
116
+ # 停止 Supabase
117
+ cd docker && bash stop.sh
118
+ ```
119
+
120
+ **常见问题**:
121
+
122
+ - ❌ `Could not find the table 'public.RxDBChange' in the schema cache`
123
+ - **原因**:Supabase 容器未启动或数据库未初始化
124
+ - **解决**:`cd docker && bash start.sh`
125
+
126
+ - ❌ `Connection refused at localhost:8000`
127
+ - **原因**:Kong API Gateway 未就绪
128
+ - **解决**:等待 `start.sh` 完成健康检查
129
+
130
+ ## 数据库 Schema
131
+
132
+ Supabase 适配器需要以下表结构:
133
+
134
+ ### RxDBChange (变更追踪表)
135
+
136
+ ```sql
137
+ CREATE TABLE "RxDBChange" (
138
+ "id" BIGSERIAL PRIMARY KEY,
139
+ "branchId" TEXT NOT NULL,
140
+ "entityType" TEXT NOT NULL,
141
+ "entityId" TEXT NOT NULL,
142
+ "type" TEXT NOT NULL, -- INSERT/UPDATE/DELETE
143
+ "patch" JSONB,
144
+ "remoteId" BIGINT,
145
+ "revertChangeId" BIGINT,
146
+ "createdAt" TIMESTAMPTZ DEFAULT NOW(),
147
+ "updatedAt" TIMESTAMPTZ DEFAULT NOW()
148
+ );
149
+ ```
150
+
151
+ ### RxDBBranch (分支元数据)
152
+
153
+ ```sql
154
+ CREATE TABLE "RxDBBranch" (
155
+ "id" TEXT PRIMARY KEY,
156
+ "dbName" TEXT NOT NULL,
157
+ "context" JSONB,
158
+ "lastPushedChangeId" BIGINT,
159
+ "lastPullRemoteChangeId" BIGINT,
160
+ "createdAt" TIMESTAMPTZ DEFAULT NOW(),
161
+ "updatedAt" TIMESTAMPTZ DEFAULT NOW()
162
+ );
163
+ ```
164
+
165
+ ### 业务表示例
166
+
167
+ ```sql
168
+ CREATE TABLE "Todo" (
169
+ "id" TEXT PRIMARY KEY,
170
+ "title" TEXT NOT NULL,
171
+ "completed" BOOLEAN DEFAULT FALSE,
172
+ "createdAt" TIMESTAMPTZ DEFAULT NOW(),
173
+ "updatedAt" TIMESTAMPTZ DEFAULT NOW()
174
+ );
175
+
176
+ -- 自动创建变更记录的触发器
177
+ CREATE TRIGGER rxdb_sync_trigger
178
+ AFTER INSERT OR UPDATE OR DELETE ON "Todo"
179
+ FOR EACH ROW EXECUTE FUNCTION rxdb_sync_change();
180
+ ```
181
+
182
+ 完整 SQL 脚本见 `docker/sql/` 目录。
183
+
184
+ ## API
185
+
186
+ ### RxDBAdapterSupabase
69
187
 
70
188
  ```typescript
71
- // May fail with bidirectional ONE_TO_ONE
72
- await userRepo.find({
73
- where: { rules: [{ field: 'idCard', operator: 'exists' }] }
74
- });
189
+ class RxDBAdapterSupabase {
190
+ constructor(rxdb: RxDB, options: SupabaseAdapterOptions);
75
191
 
76
- // ✅ Use ONE_TO_MANY instead (works correctly)
77
- await userRepo.find({
78
- where: { rules: [{ field: 'orders', operator: 'exists' }] }
79
- });
192
+ // 连接管理
193
+ connect(): Promise<IRxDBAdapter>;
194
+ disconnect(): Promise<void>;
195
+
196
+ // 同步操作
197
+ pullChanges(options: PullOptions): Promise<RemoteChange[]>;
198
+ pushChanges(changes: LocalChange[]): Promise<PushResult>;
199
+
200
+ // 批量操作
201
+ saveMany<T>(entities: T[]): Promise<T[]>;
202
+ removeMany<T>(entities: T[]): Promise<void>;
203
+ mutations(map: RxDBMutationsMap): Promise<void>;
204
+ }
80
205
  ```
81
206
 
82
- ## Testing
207
+ ### SupabaseRepository
208
+
209
+ ```typescript
210
+ class SupabaseRepository<T> {
211
+ // CRUD
212
+ create(): T;
213
+ find(options?: FindOptions): Promise<T[]>;
214
+ findOne(options?: FindOptions): Promise<T | null>;
215
+ count(options?: FindOptions): Promise<number>;
216
+
217
+ // 批量
218
+ saveMany(entities: T[]): Promise<T[]>;
219
+ removeMany(entities: T[]): Promise<void>;
220
+ }
221
+ ```
222
+
223
+ ### SupabaseTreeRepository
224
+
225
+ ```typescript
226
+ class SupabaseTreeRepository<T> extends SupabaseRepository<T> {
227
+ // 树形操作
228
+ findDescendants(entity: T, depth?: number): Promise<T[]>;
229
+ findAncestors(entity: T): Promise<T[]>;
230
+ findRoots(): Promise<T[]>;
231
+ }
232
+ ```
233
+
234
+ ## 架构
235
+
236
+ ```
237
+ ┌─────────────────────────────────────────────────────────────┐
238
+ │ RxDB Core │
239
+ │ ┌────────────┐ ┌────────────┐ ┌──────────────────────┐ │
240
+ │ │ Repository │ │VersionMgr │ │ SchemaManager │ │
241
+ │ └─────┬──────┘ └─────┬──────┘ └──────────────────────┘ │
242
+ └────────┼───────────────┼─────────────────────────────────────┘
243
+ │ │
244
+ ▼ ▼
245
+ ┌─────────────────────────────────────────────────────────────┐
246
+ │ RxDBAdapterSupabase (Remote) │
247
+ │ ┌──────────────────────────────────────────────────────┐ │
248
+ │ │ SupabaseRepository │ │
249
+ │ │ - CRUD 操作 │ │
250
+ │ │ - 查询构建 (apply_rule_group) │ │
251
+ │ │ - 批量操作 │ │
252
+ │ └──────────────────────────────────────────────────────┘ │
253
+ │ ┌──────────────────────────────────────────────────────┐ │
254
+ │ │ Sync Engine │ │
255
+ │ │ - pullChanges: 拉取远程变更 │ │
256
+ │ │ - pushChanges: 推送本地变更 (带压缩) │ │
257
+ │ │ - mergeChanges: 通过 RPC 原子性写入 │ │
258
+ │ └──────────────────────────────────────────────────────┘ │
259
+ │ ┌──────────────────────────────────────────────────────┐ │
260
+ │ │ Realtime Subscription │ │
261
+ │ │ - 监听 RxDBChange 表 INSERT 事件 │ │
262
+ │ │ - 自动触发 pull 同步 │ │
263
+ │ └──────────────────────────────────────────────────────┘ │
264
+ └─────────────┬───────────────────────────────────────────────┘
265
+
266
+
267
+ ┌─────────────────────┐
268
+ │ Supabase (PostgreSQL) │
269
+ │ - RxDBChange │
270
+ │ - RxDBBranch │
271
+ │ - Business Tables │
272
+ │ - Triggers/RPCs │
273
+ └─────────────────────┘
274
+ ```
275
+
276
+ ## 同步机制
277
+
278
+ ### Push 流程
279
+
280
+ 1. **收集本地变更**:从 `RxDBChange` 表查询 `remoteId = null` 的记录
281
+ 2. **变更压缩**:
282
+ - `INSERT→UPDATE*` → 单个 `INSERT`
283
+ - `INSERT→DELETE` → 丢弃
284
+ - `UPDATE*→DELETE` → 单个 `DELETE`
285
+ 3. **调用 RPC**:通过 `rxdb_mutations()` 原子性写入
286
+ 4. **更新元数据**:记录 `lastPushedChangeId`
287
+
288
+ ### Pull 流程
289
+
290
+ 1. **查询远程变更**:`id > lastPullRemoteChangeId` 且 `branchId != current`
291
+ 2. **应用到本地实体表**:
292
+ - `INSERT` → 插入
293
+ - `UPDATE` → 更新
294
+ - `DELETE` → 删除
295
+ 3. **写入本地 RxDBChange**:标记 `remoteId` 避免重复 push
296
+ 4. **更新元数据**:记录 `lastPullRemoteChangeId`
297
+
298
+ ## 开发
83
299
 
84
300
  ```bash
85
- nx test rxdb-adapter-supabase
301
+ # 构建
302
+ nx build rxdb-adapter-supabase
303
+
304
+ # 格式化
305
+ nx format:write --projects=rxdb-adapter-supabase
306
+
307
+ # Lint
308
+ nx lint rxdb-adapter-supabase
86
309
  ```
87
310
 
88
- Test coverage: 60/62 tests passing (96.8%)
311
+ ## License
89
312
 
90
- - ✅ 5/6 EXISTS operator tests
91
- - ⏭️ 1 skipped (bidirectional ONE_TO_ONE limitation)
313
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiao/rxdb-adapter-supabase",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@supabase/supabase-js": "^2.86.0",
29
- "@aiao/rxdb": "0.0.7"
29
+ "@aiao/rxdb": "0.0.8"
30
30
  },
31
31
  "devDependencies": {
32
32
  "dotenv": "^17.2.3",
33
- "@aiao/rxdb-test": "0.0.7",
34
- "@aiao/rxdb-adapter-sqlite": "0.0.7"
33
+ "@aiao/rxdb-test": "0.0.8",
34
+ "@aiao/rxdb-adapter-sqlite": "0.0.8"
35
35
  }
36
36
  }