@svadmin/pocketbase 0.3.0

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.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@svadmin/pocketbase",
3
+ "version": "0.3.0",
4
+ "description": "PocketBase adapters — DataProvider, AuthProvider, LiveProvider for svadmin",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "src/**/*"
9
+ ],
10
+ "main": "src/index.ts",
11
+ "types": "src/index.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ }
17
+ },
18
+ "peerDependencies": {
19
+ "@svadmin/core": "^0.3.0",
20
+ "pocketbase": ">=0.21.0"
21
+ },
22
+ "keywords": ["svadmin", "pocketbase", "data-provider", "auth-provider", "live-provider"],
23
+ "license": "MIT",
24
+ "author": "zuohuadong",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/zuohuadong/svadmin.git"
28
+ },
29
+ "homepage": "https://github.com/zuohuadong/svadmin#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/zuohuadong/svadmin/issues"
32
+ }
33
+ }
@@ -0,0 +1,120 @@
1
+ // @svadmin/pocketbase — PocketBase AuthProvider
2
+ import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
3
+
4
+ interface PocketBaseAuthOptions {
5
+ /** PocketBase client instance */
6
+ pb: unknown; // PocketBase
7
+ }
8
+
9
+ // Duck-typed PocketBase client with auth methods
10
+ interface PBClientAuth {
11
+ authStore: {
12
+ isValid: boolean;
13
+ model: { id: string; name?: string; email: string; avatar?: string; [key: string]: unknown } | null;
14
+ clear: () => void;
15
+ token: string;
16
+ };
17
+ collection: (name: string) => {
18
+ authWithPassword: (email: string, password: string) => Promise<{ record: Record<string, unknown>; token: string }>;
19
+ create: (data: Record<string, unknown>) => Promise<Record<string, unknown>>;
20
+ requestPasswordReset: (email: string) => Promise<boolean>;
21
+ confirmPasswordReset: (token: string, password: string, passwordConfirm: string) => Promise<boolean>;
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Create an AuthProvider backed by PocketBase.
27
+ *
28
+ * Usage:
29
+ * ```ts
30
+ * import PocketBase from 'pocketbase';
31
+ * import { createPocketBaseAuthProvider } from '@svadmin/pocketbase';
32
+ *
33
+ * const pb = new PocketBase('http://127.0.0.1:8090');
34
+ * const authProvider = createPocketBaseAuthProvider({ pb });
35
+ * ```
36
+ */
37
+ export function createPocketBaseAuthProvider(options: PocketBaseAuthOptions): AuthProvider {
38
+ const pb = options.pb as PBClientAuth;
39
+
40
+ return {
41
+ async login({ email, password, collection }: Record<string, unknown>): Promise<AuthActionResult> {
42
+ try {
43
+ const col = (collection as string) || 'users';
44
+ await pb.collection(col).authWithPassword(email as string, password as string);
45
+ return { success: true, redirectTo: '/' };
46
+ } catch (err) {
47
+ return { success: false, error: { message: (err as Error).message } };
48
+ }
49
+ },
50
+
51
+ async logout(): Promise<AuthActionResult> {
52
+ pb.authStore.clear();
53
+ return { success: true, redirectTo: '/login' };
54
+ },
55
+
56
+ async check(): Promise<CheckResult> {
57
+ if (pb.authStore.isValid && pb.authStore.model) {
58
+ return { authenticated: true };
59
+ }
60
+ return { authenticated: false, redirectTo: '/login', logout: true };
61
+ },
62
+
63
+ async getIdentity(): Promise<Identity | null> {
64
+ const model = pb.authStore.model;
65
+ if (!model) return null;
66
+ return {
67
+ id: model.id,
68
+ name: model.name || model.email.split('@')[0],
69
+ email: model.email,
70
+ avatar: model.avatar,
71
+ };
72
+ },
73
+
74
+ async register({ email, password, passwordConfirm, name, collection }: Record<string, unknown>): Promise<AuthActionResult> {
75
+ try {
76
+ const col = (collection as string) || 'users';
77
+ await pb.collection(col).create({
78
+ email: email as string,
79
+ password: password as string,
80
+ passwordConfirm: (passwordConfirm ?? password) as string,
81
+ name: name as string | undefined,
82
+ });
83
+ return { success: true, redirectTo: '/login' };
84
+ } catch (err) {
85
+ return { success: false, error: { message: (err as Error).message } };
86
+ }
87
+ },
88
+
89
+ async forgotPassword({ email, collection }: Record<string, unknown>): Promise<AuthActionResult> {
90
+ try {
91
+ const col = (collection as string) || 'users';
92
+ await pb.collection(col).requestPasswordReset(email as string);
93
+ return { success: true };
94
+ } catch (err) {
95
+ return { success: false, error: { message: (err as Error).message } };
96
+ }
97
+ },
98
+
99
+ async updatePassword({ token, password, passwordConfirm, collection }: Record<string, unknown>): Promise<AuthActionResult> {
100
+ try {
101
+ const col = (collection as string) || 'users';
102
+ await pb.collection(col).confirmPasswordReset(
103
+ token as string,
104
+ password as string,
105
+ (passwordConfirm ?? password) as string,
106
+ );
107
+ return { success: true, redirectTo: '/' };
108
+ } catch (err) {
109
+ return { success: false, error: { message: (err as Error).message } };
110
+ }
111
+ },
112
+
113
+ async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
114
+ if (error instanceof Error && (error.message.includes('401') || error.message.includes('403'))) {
115
+ return { redirectTo: '/login', logout: true };
116
+ }
117
+ return {};
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,122 @@
1
+ // @svadmin/pocketbase — PocketBase DataProvider
2
+ // Maps svadmin CRUD operations to PocketBase SDK
3
+
4
+ import type {
5
+ DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
6
+ CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
7
+ GetManyParams, GetManyResult, DeleteManyParams, DeleteManyResult, BaseRecord,
8
+ } from '@svadmin/core';
9
+
10
+ interface PocketBaseProviderOptions {
11
+ /** PocketBase client instance */
12
+ pb: unknown; // PocketBase
13
+ }
14
+
15
+ // Duck-typed PocketBase collection interface
16
+ interface PBCollection {
17
+ getList: (page: number, perPage: number, options?: { sort?: string; filter?: string }) => Promise<{ items: Record<string, unknown>[]; totalItems: number }>;
18
+ getOne: (id: string) => Promise<Record<string, unknown>>;
19
+ getFullList: (options?: { sort?: string; filter?: string; batch?: number }) => Promise<Record<string, unknown>[]>;
20
+ create: (data: Record<string, unknown>) => Promise<Record<string, unknown>>;
21
+ update: (id: string, data: Record<string, unknown>) => Promise<Record<string, unknown>>;
22
+ delete: (id: string) => Promise<boolean>;
23
+ }
24
+
25
+ interface PBClient {
26
+ collection: (name: string) => PBCollection;
27
+ buildUrl: (path: string) => string;
28
+ }
29
+
30
+ function buildSort(sorters?: { field: string; order: 'asc' | 'desc' }[]): string | undefined {
31
+ if (!sorters?.length) return undefined;
32
+ return sorters.map(s => s.order === 'desc' ? `-${s.field}` : s.field).join(',');
33
+ }
34
+
35
+ function buildFilter(filters?: { field: string; operator: string; value: unknown }[]): string | undefined {
36
+ if (!filters?.length) return undefined;
37
+ return filters.map(f => {
38
+ const v = f.value;
39
+ switch (f.operator) {
40
+ case 'eq': return `${f.field} = '${v}'`;
41
+ case 'ne': return `${f.field} != '${v}'`;
42
+ case 'contains': return `${f.field} ~ '${v}'`;
43
+ case 'ncontains': return `${f.field} !~ '${v}'`;
44
+ case 'gt': return `${f.field} > '${v}'`;
45
+ case 'gte': return `${f.field} >= '${v}'`;
46
+ case 'lt': return `${f.field} < '${v}'`;
47
+ case 'lte': return `${f.field} <= '${v}'`;
48
+ case 'null': return `${f.field} = ''`;
49
+ case 'nnull': return `${f.field} != ''`;
50
+ case 'in': return `${f.field} ?= '${(v as unknown[]).join("' || " + f.field + " ?= '")}'`;
51
+ default: return `${f.field} = '${v}'`;
52
+ }
53
+ }).join(' && ');
54
+ }
55
+
56
+ /**
57
+ * Create a DataProvider backed by PocketBase.
58
+ *
59
+ * Usage:
60
+ * ```ts
61
+ * import PocketBase from 'pocketbase';
62
+ * import { createPocketBaseDataProvider } from '@svadmin/pocketbase';
63
+ *
64
+ * const pb = new PocketBase('http://127.0.0.1:8090');
65
+ * const dataProvider = createPocketBaseDataProvider({ pb });
66
+ * ```
67
+ */
68
+ export function createPocketBaseDataProvider(options: PocketBaseProviderOptions): DataProvider {
69
+ const pb = options.pb as PBClient;
70
+
71
+ return {
72
+ getApiUrl: () => pb.buildUrl('/api'),
73
+
74
+ async getList<TData extends BaseRecord = BaseRecord>(params: GetListParams): Promise<GetListResult<TData>> {
75
+ const page = params.pagination?.current ?? 1;
76
+ const perPage = params.pagination?.pageSize ?? 10;
77
+ const result = await pb.collection(params.resource).getList(page, perPage, {
78
+ sort: buildSort(params.sorters),
79
+ filter: buildFilter(params.filters),
80
+ });
81
+ return {
82
+ data: result.items as TData[],
83
+ total: result.totalItems,
84
+ };
85
+ },
86
+
87
+ async getOne<TData extends BaseRecord = BaseRecord>(params: GetOneParams): Promise<GetOneResult<TData>> {
88
+ const record = await pb.collection(params.resource).getOne(String(params.id));
89
+ return { data: record as TData };
90
+ },
91
+
92
+ async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>): Promise<CreateResult<TData>> {
93
+ const record = await pb.collection(params.resource).create(params.variables as Record<string, unknown>);
94
+ return { data: record as TData };
95
+ },
96
+
97
+ async update<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
98
+ const record = await pb.collection(params.resource).update(String(params.id), params.variables as Record<string, unknown>);
99
+ return { data: record as TData };
100
+ },
101
+
102
+ async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
103
+ await pb.collection(params.resource).delete(String(params.id));
104
+ return { data: { id: params.id } as unknown as TData };
105
+ },
106
+
107
+ async getMany<TData extends BaseRecord = BaseRecord>(params: GetManyParams): Promise<GetManyResult<TData>> {
108
+ const filter = params.ids.map(id => `id = '${id}'`).join(' || ');
109
+ const records = await pb.collection(params.resource).getFullList({ filter });
110
+ return { data: records as TData[] };
111
+ },
112
+
113
+ async deleteMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
114
+ const results: unknown[] = [];
115
+ for (const id of params.ids) {
116
+ await pb.collection(params.resource).delete(String(id));
117
+ results.push({ id });
118
+ }
119
+ return { data: results as TData[] };
120
+ },
121
+ };
122
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // @svadmin/pocketbase — PocketBase adapters
2
+
3
+ export { createPocketBaseDataProvider } from './data-provider';
4
+ export { createPocketBaseAuthProvider } from './auth-provider';
5
+ export { createPocketBaseLiveProvider } from './live-provider';
@@ -0,0 +1,65 @@
1
+ // @svadmin/pocketbase — PocketBase LiveProvider (SSE Realtime)
2
+ import type { LiveProvider, LiveEvent } from '@svadmin/core';
3
+
4
+ interface PocketBaseLiveOptions {
5
+ /** PocketBase client instance */
6
+ pb: unknown; // PocketBase
7
+ }
8
+
9
+ // Duck-typed PocketBase realtime interface
10
+ interface PBClientRealtime {
11
+ collection: (name: string) => {
12
+ subscribe: (topic: string, callback: (data: PBRealtimeEvent) => void) => Promise<void>;
13
+ unsubscribe: (topic?: string) => Promise<void>;
14
+ };
15
+ }
16
+
17
+ interface PBRealtimeEvent {
18
+ action: 'create' | 'update' | 'delete';
19
+ record: Record<string, unknown>;
20
+ }
21
+
22
+ /**
23
+ * Map PocketBase realtime action to svadmin LiveEvent type.
24
+ */
25
+ function mapAction(action: string): LiveEvent['type'] {
26
+ switch (action) {
27
+ case 'create': return 'INSERT';
28
+ case 'update': return 'UPDATE';
29
+ case 'delete': return 'DELETE';
30
+ default: return 'UPDATE';
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Create a LiveProvider backed by PocketBase Realtime (SSE).
36
+ *
37
+ * Usage:
38
+ * ```ts
39
+ * import PocketBase from 'pocketbase';
40
+ * import { createPocketBaseLiveProvider } from '@svadmin/pocketbase';
41
+ *
42
+ * const pb = new PocketBase('http://127.0.0.1:8090');
43
+ * const liveProvider = createPocketBaseLiveProvider({ pb });
44
+ * ```
45
+ */
46
+ export function createPocketBaseLiveProvider(options: PocketBaseLiveOptions): LiveProvider {
47
+ const pb = options.pb as PBClientRealtime;
48
+
49
+ return {
50
+ subscribe({ resource, callback }) {
51
+ // PocketBase subscribe uses '*' for all events on a collection
52
+ pb.collection(resource).subscribe('*', (data) => {
53
+ callback({
54
+ type: mapAction(data.action),
55
+ resource,
56
+ payload: data.record,
57
+ });
58
+ });
59
+
60
+ return () => {
61
+ pb.collection(resource).unsubscribe('*');
62
+ };
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,212 @@
1
+ // @svadmin/pocketbase — Unit Tests
2
+ import { describe, test, expect, mock } from 'bun:test';
3
+ import { createPocketBaseDataProvider } from './data-provider';
4
+ import { createPocketBaseAuthProvider } from './auth-provider';
5
+ import { createPocketBaseLiveProvider } from './live-provider';
6
+
7
+ // ─── Mock PocketBase Client ─────────────────────────────────
8
+
9
+ function createMockPB() {
10
+ const mockCollection = {
11
+ getList: mock(async () => ({
12
+ items: [{ id: '1', name: 'Test' }, { id: '2', name: 'Test2' }],
13
+ totalItems: 2,
14
+ })),
15
+ getOne: mock(async (id: string) => ({ id, name: 'Test' })),
16
+ getFullList: mock(async () => [{ id: '1' }, { id: '2' }]),
17
+ create: mock(async (data: Record<string, unknown>) => ({ id: 'new-1', ...data })),
18
+ update: mock(async (id: string, data: Record<string, unknown>) => ({ id, ...data })),
19
+ delete: mock(async () => true),
20
+ subscribe: mock(async () => {}),
21
+ unsubscribe: mock(async () => {}),
22
+ authWithPassword: mock(async () => ({ record: { id: 'user-1' }, token: 'tok' })),
23
+ requestPasswordReset: mock(async () => true),
24
+ confirmPasswordReset: mock(async () => true),
25
+ };
26
+
27
+ return {
28
+ pb: {
29
+ collection: mock(() => mockCollection),
30
+ buildUrl: mock((path: string) => `http://localhost:8090${path}`),
31
+ authStore: {
32
+ isValid: true,
33
+ model: { id: 'user-1', name: 'Admin', email: 'admin@test.com' },
34
+ clear: mock(() => {}),
35
+ token: 'test-token',
36
+ },
37
+ },
38
+ mockCollection,
39
+ };
40
+ }
41
+
42
+ // ─── DataProvider Tests ─────────────────────────────────────
43
+
44
+ describe('PocketBase DataProvider', () => {
45
+ test('getList returns data with total', async () => {
46
+ const { pb } = createMockPB();
47
+ const dp = createPocketBaseDataProvider({ pb });
48
+ const result = await dp.getList({ resource: 'posts' });
49
+ expect(result.data).toHaveLength(2);
50
+ expect(result.total).toBe(2);
51
+ });
52
+
53
+ test('getList passes pagination', async () => {
54
+ const { pb, mockCollection } = createMockPB();
55
+ const dp = createPocketBaseDataProvider({ pb });
56
+ await dp.getList({ resource: 'posts', pagination: { current: 3, pageSize: 20 } });
57
+ expect(mockCollection.getList).toHaveBeenCalledWith(3, 20, expect.anything());
58
+ });
59
+
60
+ test('getList passes sorters', async () => {
61
+ const { pb, mockCollection } = createMockPB();
62
+ const dp = createPocketBaseDataProvider({ pb });
63
+ await dp.getList({ resource: 'posts', sorters: [{ field: 'name', order: 'desc' }] });
64
+ const opts = mockCollection.getList.mock.calls[0][2] as { sort?: string };
65
+ expect(opts.sort).toBe('-name');
66
+ });
67
+
68
+ test('getList passes filters', async () => {
69
+ const { pb, mockCollection } = createMockPB();
70
+ const dp = createPocketBaseDataProvider({ pb });
71
+ await dp.getList({ resource: 'posts', filters: [{ field: 'status', operator: 'eq', value: 'active' }] });
72
+ const opts = mockCollection.getList.mock.calls[0][2] as { filter?: string };
73
+ expect(opts.filter).toContain("status = 'active'");
74
+ });
75
+
76
+ test('getOne returns single record', async () => {
77
+ const { pb } = createMockPB();
78
+ const dp = createPocketBaseDataProvider({ pb });
79
+ const result = await dp.getOne({ resource: 'posts', id: '1' });
80
+ expect(result.data).toHaveProperty('id', '1');
81
+ });
82
+
83
+ test('create returns new record', async () => {
84
+ const { pb } = createMockPB();
85
+ const dp = createPocketBaseDataProvider({ pb });
86
+ const result = await dp.create({ resource: 'posts', variables: { name: 'New' } });
87
+ expect(result.data).toHaveProperty('name', 'New');
88
+ });
89
+
90
+ test('update returns updated record', async () => {
91
+ const { pb } = createMockPB();
92
+ const dp = createPocketBaseDataProvider({ pb });
93
+ const result = await dp.update({ resource: 'posts', id: '1', variables: { name: 'Updated' } });
94
+ expect(result.data).toHaveProperty('name', 'Updated');
95
+ });
96
+
97
+ test('deleteOne calls delete', async () => {
98
+ const { pb, mockCollection } = createMockPB();
99
+ const dp = createPocketBaseDataProvider({ pb });
100
+ await dp.deleteOne({ resource: 'posts', id: '1' });
101
+ expect(mockCollection.delete).toHaveBeenCalledWith('1');
102
+ });
103
+
104
+ test('getMany fetches all ids', async () => {
105
+ const { pb, mockCollection } = createMockPB();
106
+ const dp = createPocketBaseDataProvider({ pb });
107
+ const result = await dp.getMany!({ resource: 'posts', ids: ['1', '2'] });
108
+ expect(result.data).toHaveLength(2);
109
+ const filterArg = mockCollection.getFullList.mock.calls[0][0] as { filter?: string };
110
+ expect(filterArg.filter).toContain("id = '1'");
111
+ });
112
+
113
+ test('deleteMany deletes each', async () => {
114
+ const { pb, mockCollection } = createMockPB();
115
+ const dp = createPocketBaseDataProvider({ pb });
116
+ await dp.deleteMany!({ resource: 'posts', ids: ['1', '2'] });
117
+ expect(mockCollection.delete).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ test('getApiUrl returns correct url', () => {
121
+ const { pb } = createMockPB();
122
+ const dp = createPocketBaseDataProvider({ pb });
123
+ expect(dp.getApiUrl()).toBe('http://localhost:8090/api');
124
+ });
125
+ });
126
+
127
+ // ─── AuthProvider Tests ─────────────────────────────────────
128
+
129
+ describe('PocketBase AuthProvider', () => {
130
+ test('login success', async () => {
131
+ const { pb } = createMockPB();
132
+ const auth = createPocketBaseAuthProvider({ pb });
133
+ const result = await auth.login({ email: 'admin@test.com', password: 'pass' });
134
+ expect(result.success).toBe(true);
135
+ expect(result.redirectTo).toBe('/');
136
+ });
137
+
138
+ test('login failure', async () => {
139
+ const { pb, mockCollection } = createMockPB();
140
+ mockCollection.authWithPassword = mock(async () => { throw new Error('Invalid credentials'); });
141
+ const auth = createPocketBaseAuthProvider({ pb });
142
+ const result = await auth.login({ email: 'bad', password: 'bad' });
143
+ expect(result.success).toBe(false);
144
+ expect(result.error?.message).toBe('Invalid credentials');
145
+ });
146
+
147
+ test('logout clears auth store', async () => {
148
+ const { pb } = createMockPB();
149
+ const auth = createPocketBaseAuthProvider({ pb });
150
+ const result = await auth.logout();
151
+ expect(result.success).toBe(true);
152
+ expect(pb.authStore.clear).toHaveBeenCalledTimes(1);
153
+ });
154
+
155
+ test('check returns authenticated when valid', async () => {
156
+ const { pb } = createMockPB();
157
+ const auth = createPocketBaseAuthProvider({ pb });
158
+ const result = await auth.check();
159
+ expect(result.authenticated).toBe(true);
160
+ });
161
+
162
+ test('check returns not authenticated when invalid', async () => {
163
+ const { pb } = createMockPB();
164
+ pb.authStore.isValid = false;
165
+ const auth = createPocketBaseAuthProvider({ pb });
166
+ const result = await auth.check();
167
+ expect(result.authenticated).toBe(false);
168
+ });
169
+
170
+ test('getIdentity returns user', async () => {
171
+ const { pb } = createMockPB();
172
+ const auth = createPocketBaseAuthProvider({ pb });
173
+ const identity = await auth.getIdentity();
174
+ expect(identity).not.toBeNull();
175
+ expect(identity!.name).toBe('Admin');
176
+ });
177
+
178
+ test('getIdentity returns null when no model', async () => {
179
+ const { pb } = createMockPB();
180
+ pb.authStore.model = null as unknown as typeof pb.authStore.model;
181
+ const auth = createPocketBaseAuthProvider({ pb });
182
+ const identity = await auth.getIdentity();
183
+ expect(identity).toBeNull();
184
+ });
185
+
186
+ test('register success', async () => {
187
+ const { pb } = createMockPB();
188
+ const auth = createPocketBaseAuthProvider({ pb });
189
+ const result = await auth.register!({ email: 'new@test.com', password: 'pass', passwordConfirm: 'pass' });
190
+ expect(result.success).toBe(true);
191
+ });
192
+ });
193
+
194
+ // ─── LiveProvider Tests ─────────────────────────────────────
195
+
196
+ describe('PocketBase LiveProvider', () => {
197
+ test('subscribe calls pb.collection().subscribe', () => {
198
+ const { pb, mockCollection } = createMockPB();
199
+ const lp = createPocketBaseLiveProvider({ pb });
200
+ const cb = mock(() => {});
201
+ lp.subscribe({ resource: 'posts', callback: cb });
202
+ expect(mockCollection.subscribe).toHaveBeenCalledWith('*', expect.any(Function));
203
+ });
204
+
205
+ test('unsubscribe calls pb.collection().unsubscribe', () => {
206
+ const { pb, mockCollection } = createMockPB();
207
+ const lp = createPocketBaseLiveProvider({ pb });
208
+ const unsub = lp.subscribe({ resource: 'posts', callback: () => {} });
209
+ unsub();
210
+ expect(mockCollection.unsubscribe).toHaveBeenCalledWith('*');
211
+ });
212
+ });