@svadmin/pocketbase 0.10.7 → 0.11.7
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 +9 -3
- package/src/auth-provider.ts +138 -76
- package/src/boundary.ts +30 -0
- package/src/data-provider.ts +52 -37
- package/src/index.ts +3 -1
- package/src/live-provider.ts +87 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/pocketbase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "PocketBase adapters — DataProvider, AuthProvider, LiveProvider for svadmin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,9 +20,15 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.
|
|
23
|
+
"@svadmin/core": "^0.53.0",
|
|
24
24
|
"pocketbase": ">=0.28.0"
|
|
25
25
|
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@sinclair/typebox": "^0.34.52"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"pocketbase": "0.28.0"
|
|
31
|
+
},
|
|
26
32
|
"keywords": [
|
|
27
33
|
"svadmin",
|
|
28
34
|
"pocketbase",
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
"author": "zuohuadong",
|
|
35
41
|
"repository": {
|
|
36
42
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/vibeunion/svadmin.git",
|
|
43
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
38
44
|
"directory": "packages/pocketbase"
|
|
39
45
|
},
|
|
40
46
|
"homepage": "https://github.com/vibeunion/svadmin#readme",
|
package/src/auth-provider.ts
CHANGED
|
@@ -1,117 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
2
3
|
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
4
|
+
import { authRecord, collectionName, decode, nonempty, PocketBaseBoundaryError } from './boundary';
|
|
3
5
|
|
|
4
|
-
interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
6
|
+
export interface PocketBaseAuthClient {
|
|
7
|
+
readonly authStore: {
|
|
8
|
+
readonly isValid: unknown;
|
|
9
|
+
readonly record: unknown;
|
|
10
|
+
readonly token: unknown;
|
|
11
|
+
clear(): void;
|
|
16
12
|
};
|
|
17
|
-
collection
|
|
18
|
-
authWithPassword
|
|
19
|
-
create
|
|
20
|
-
requestPasswordReset
|
|
21
|
-
confirmPasswordReset
|
|
13
|
+
collection(name: string): {
|
|
14
|
+
authWithPassword(email: string, password: string): Promise<unknown>;
|
|
15
|
+
create(data: Record<string, unknown>): Promise<unknown>;
|
|
16
|
+
requestPasswordReset(email: string): Promise<unknown>;
|
|
17
|
+
confirmPasswordReset(token: string, password: string, passwordConfirm: string): Promise<unknown>;
|
|
22
18
|
};
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
export interface PocketBaseAuthOptions {
|
|
22
|
+
pb: PocketBaseAuthClient;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const collection = Type.Optional(collectionName);
|
|
26
|
+
const username = Type.Optional(nonempty);
|
|
27
|
+
const credentials = Type.Object({ email: nonempty, password: nonempty, username, collection }, { additionalProperties: false });
|
|
28
|
+
const registration = Type.Object({
|
|
29
|
+
email: nonempty, password: nonempty, username,
|
|
30
|
+
confirmPassword: Type.Optional(nonempty), name: Type.Optional(Type.String()), collection,
|
|
31
|
+
}, { additionalProperties: false });
|
|
32
|
+
const resetRequest = Type.Object({ email: nonempty, username, collection }, { additionalProperties: false });
|
|
33
|
+
const resetConfirmation = Type.Object({
|
|
34
|
+
token: nonempty, password: nonempty, confirmPassword: Type.Optional(nonempty), collection,
|
|
35
|
+
}, { additionalProperties: false });
|
|
36
|
+
const authResponse = Type.Object({ record: authRecord, token: nonempty });
|
|
37
|
+
const session = Type.Object({ isValid: Type.Literal(true), record: authRecord, token: nonempty });
|
|
38
|
+
const confirmed = Type.Literal(true);
|
|
39
|
+
|
|
40
|
+
function validateIdentifier(input: { email: string; username?: string }): void {
|
|
41
|
+
if (input.username !== undefined && input.username !== input.email) throw new PocketBaseBoundaryError('input');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function failure(operation: string, error: unknown, write = false): AuthActionResult {
|
|
45
|
+
if (error instanceof PocketBaseBoundaryError) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: {
|
|
49
|
+
name: write && error.phase === 'response' ? 'WRITE_OUTCOME_UNKNOWN' : 'INVALID_AUTH_DATA',
|
|
50
|
+
message: write && error.phase === 'response'
|
|
51
|
+
? `${operation} response was invalid; the operation may have succeeded.`
|
|
52
|
+
: error.message,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { success: false, error: { message: `${operation} failed.` } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** SDK return values and auth-store records enter the application only after validation. */
|
|
60
|
+
export function createPocketBaseAuthProvider({ pb }: PocketBaseAuthOptions): AuthProvider {
|
|
61
|
+
function currentRecord() {
|
|
62
|
+
try {
|
|
63
|
+
const isValid: unknown = pb.authStore.isValid;
|
|
64
|
+
if (isValid === false) return null;
|
|
65
|
+
return decode(session, {
|
|
66
|
+
isValid, record: pb.authStore.record, token: pb.authStore.token,
|
|
67
|
+
}, 'session').record;
|
|
68
|
+
} catch {
|
|
69
|
+
pb.authStore.clear();
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
39
73
|
|
|
40
74
|
return {
|
|
41
|
-
async login(
|
|
75
|
+
async login(params): Promise<AuthActionResult> {
|
|
42
76
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
77
|
+
const input = decode(credentials, params, 'input');
|
|
78
|
+
validateIdentifier(input);
|
|
79
|
+
const result: unknown = await pb.collection(input.collection ?? 'users')
|
|
80
|
+
.authWithPassword(input.email, input.password);
|
|
81
|
+
try {
|
|
82
|
+
decode(authResponse, result, 'response');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
pb.authStore.clear();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
45
87
|
return { success: true, redirectTo: '/' };
|
|
46
|
-
} catch (
|
|
47
|
-
return
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return failure('Login', error);
|
|
48
90
|
}
|
|
49
91
|
},
|
|
50
92
|
|
|
51
93
|
async logout(): Promise<AuthActionResult> {
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
try {
|
|
95
|
+
pb.authStore.clear();
|
|
96
|
+
return { success: true, redirectTo: '/login' };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return failure('Logout', error);
|
|
99
|
+
}
|
|
54
100
|
},
|
|
55
101
|
|
|
56
102
|
async check(): Promise<CheckResult> {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
103
|
+
return currentRecord()
|
|
104
|
+
? { authenticated: true }
|
|
105
|
+
: { authenticated: false, redirectTo: '/login', logout: true };
|
|
61
106
|
},
|
|
62
107
|
|
|
63
108
|
async getIdentity(): Promise<Identity | null> {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
66
|
-
return {
|
|
67
|
-
id:
|
|
68
|
-
name:
|
|
69
|
-
email:
|
|
70
|
-
avatar:
|
|
71
|
-
};
|
|
109
|
+
const record = currentRecord();
|
|
110
|
+
if (!record) return null;
|
|
111
|
+
return definedOptions({
|
|
112
|
+
id: record.id,
|
|
113
|
+
name: record.name || record.email?.split('@')[0] || record.id,
|
|
114
|
+
email: record.email,
|
|
115
|
+
avatar: record.avatar,
|
|
116
|
+
});
|
|
72
117
|
},
|
|
73
118
|
|
|
74
|
-
async register(
|
|
119
|
+
async register(params): Promise<AuthActionResult> {
|
|
75
120
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
const input = decode(registration, params, 'input');
|
|
122
|
+
validateIdentifier(input);
|
|
123
|
+
if (input.confirmPassword !== undefined && input.confirmPassword !== input.password) {
|
|
124
|
+
throw new PocketBaseBoundaryError('input');
|
|
125
|
+
}
|
|
126
|
+
const result: unknown = await pb.collection(input.collection ?? 'users').create(definedOptions({
|
|
127
|
+
email: input.email, password: input.password,
|
|
128
|
+
passwordConfirm: input.confirmPassword ?? input.password, name: input.name,
|
|
129
|
+
}));
|
|
130
|
+
decode(authRecord, result, 'response');
|
|
83
131
|
return { success: true, redirectTo: '/login' };
|
|
84
|
-
} catch (
|
|
85
|
-
return
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return failure('Registration', error, true);
|
|
86
134
|
}
|
|
87
135
|
},
|
|
88
136
|
|
|
89
|
-
async forgotPassword(
|
|
137
|
+
async forgotPassword(params): Promise<AuthActionResult> {
|
|
90
138
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
139
|
+
const input = decode(resetRequest, params, 'input');
|
|
140
|
+
validateIdentifier(input);
|
|
141
|
+
const result: unknown = await pb.collection(input.collection ?? 'users').requestPasswordReset(input.email);
|
|
142
|
+
decode(confirmed, result, 'response');
|
|
93
143
|
return { success: true };
|
|
94
|
-
} catch (
|
|
95
|
-
return
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return failure('Password reset request', error, true);
|
|
96
146
|
}
|
|
97
147
|
},
|
|
98
148
|
|
|
99
|
-
async updatePassword(
|
|
149
|
+
async updatePassword(params): Promise<AuthActionResult> {
|
|
100
150
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
const input = decode(resetConfirmation, params, 'input');
|
|
152
|
+
if (input.confirmPassword !== undefined && input.confirmPassword !== input.password) {
|
|
153
|
+
throw new PocketBaseBoundaryError('input');
|
|
154
|
+
}
|
|
155
|
+
const result: unknown = await pb.collection(input.collection ?? 'users').confirmPasswordReset(
|
|
156
|
+
input.token, input.password, input.confirmPassword ?? input.password,
|
|
106
157
|
);
|
|
158
|
+
decode(confirmed, result, 'response');
|
|
107
159
|
return { success: true, redirectTo: '/' };
|
|
108
|
-
} catch (
|
|
109
|
-
return
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return failure('Password reset', error, true);
|
|
110
162
|
}
|
|
111
163
|
},
|
|
112
164
|
|
|
113
|
-
async onError(error
|
|
114
|
-
|
|
165
|
+
async onError(error): Promise<{ redirectTo?: string; logout?: boolean }> {
|
|
166
|
+
// SDK errors are class instances. Only the numeric status is projected.
|
|
167
|
+
let status: unknown;
|
|
168
|
+
try {
|
|
169
|
+
const descriptor = typeof error === 'object' && error !== null
|
|
170
|
+
? Object.getOwnPropertyDescriptor(error, 'status') : undefined;
|
|
171
|
+
status = descriptor && 'value' in descriptor ? descriptor.value : undefined;
|
|
172
|
+
} catch {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
if (status === 401 || status === 403) {
|
|
176
|
+
pb.authStore.clear();
|
|
115
177
|
return { redirectTo: '/login', logout: true };
|
|
116
178
|
}
|
|
117
179
|
return {};
|
package/src/boundary.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from '@sinclair/typebox';
|
|
2
|
+
import { checkExact, snapshotPlainData } from '@svadmin/core/schema';
|
|
3
|
+
|
|
4
|
+
export const nonempty = Type.String({ minLength: 1 });
|
|
5
|
+
export const collectionName = Type.String({ minLength: 1, pattern: '^[A-Za-z0-9_]+$' });
|
|
6
|
+
export const authRecord = Type.Object({
|
|
7
|
+
id: nonempty,
|
|
8
|
+
name: Type.Optional(Type.String()),
|
|
9
|
+
email: Type.Optional(Type.String()),
|
|
10
|
+
avatar: Type.Optional(Type.String()),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export class PocketBaseBoundaryError extends Error {
|
|
14
|
+
constructor(readonly phase: 'input' | 'response' | 'session' | 'event') {
|
|
15
|
+
super(`Invalid PocketBase ${phase}`);
|
|
16
|
+
this.name = 'PocketBaseBoundaryError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decode<S extends TSchema>(
|
|
21
|
+
schema: S, value: unknown, phase: PocketBaseBoundaryError['phase'],
|
|
22
|
+
): Static<S> {
|
|
23
|
+
try {
|
|
24
|
+
const candidate = snapshotPlainData(value);
|
|
25
|
+
if (checkExact(schema, candidate)) return candidate;
|
|
26
|
+
} catch {
|
|
27
|
+
// Never expose submitted credentials, SDK payloads, or accessor exceptions.
|
|
28
|
+
}
|
|
29
|
+
throw new PocketBaseBoundaryError(phase);
|
|
30
|
+
}
|
package/src/data-provider.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
// @svadmin/pocketbase — PocketBase DataProvider
|
|
3
2
|
// Maps svadmin CRUD operations to PocketBase SDK
|
|
4
3
|
|
|
5
4
|
import type {
|
|
6
|
-
DataProvider,
|
|
7
|
-
CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
|
|
8
|
-
GetManyParams, GetManyResult, DeleteManyParams, DeleteManyResult, BaseRecord,
|
|
5
|
+
DataProvider,
|
|
9
6
|
} from '@svadmin/core';
|
|
7
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
8
|
+
import { withValidatedResponses, type DataTransport } from '@svadmin/core/schema';
|
|
10
9
|
|
|
11
10
|
interface PocketBaseProviderOptions {
|
|
12
11
|
/** PocketBase client instance */
|
|
13
|
-
pb:
|
|
12
|
+
pb: PBClient;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
//
|
|
15
|
+
// SDK results remain unknown until the shared response boundary validates them.
|
|
17
16
|
interface PBCollection {
|
|
18
|
-
getList: (page: number, perPage: number, options?: { sort?: string; filter?: string }) => Promise<
|
|
19
|
-
getOne: (id: string) => Promise<
|
|
20
|
-
getFullList: (options?: { sort?: string; filter?: string; batch?: number }) => Promise<
|
|
21
|
-
create: (data: Record<string, unknown>) => Promise<
|
|
22
|
-
update: (id: string, data: Record<string, unknown>) => Promise<
|
|
23
|
-
delete: (id: string) => Promise<
|
|
17
|
+
getList: (page: number, perPage: number, options?: { sort?: string; filter?: string }) => Promise<unknown>;
|
|
18
|
+
getOne: (id: string) => Promise<unknown>;
|
|
19
|
+
getFullList: (options?: { sort?: string; filter?: string; batch?: number }) => Promise<unknown>;
|
|
20
|
+
create: (data: Record<string, unknown>) => Promise<unknown>;
|
|
21
|
+
update: (id: string, data: Record<string, unknown>) => Promise<unknown>;
|
|
22
|
+
delete: (id: string) => Promise<unknown>;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
interface PBClient {
|
|
@@ -28,6 +27,15 @@ interface PBClient {
|
|
|
28
27
|
buildUrl: (path: string) => string;
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inputRecord(value: unknown): Record<string, unknown> {
|
|
35
|
+
if (!isObject(value)) throw new TypeError('PocketBase variables must be an object');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
function buildSort(sorters?: { field: string; order: 'asc' | 'desc' }[]): string | undefined {
|
|
32
40
|
if (!sorters?.length) return undefined;
|
|
33
41
|
return sorters.map(s => s.order === 'desc' ? `-${s.field}` : s.field).join(',');
|
|
@@ -48,7 +56,9 @@ function buildFilter(filters?: { field: string; operator: string; value: unknown
|
|
|
48
56
|
case 'lte': return `${f.field} <= '${v}'`;
|
|
49
57
|
case 'null': return `${f.field} = ''`;
|
|
50
58
|
case 'nnull': return `${f.field} != ''`;
|
|
51
|
-
case 'in':
|
|
59
|
+
case 'in':
|
|
60
|
+
if (!Array.isArray(v)) throw new TypeError('PocketBase in filter requires an array');
|
|
61
|
+
return `${f.field} ?= '${v.join("' || " + f.field + " ?= '")}'`;
|
|
52
62
|
default: return `${f.field} = '${v}'`;
|
|
53
63
|
}
|
|
54
64
|
}).join(' && ');
|
|
@@ -67,57 +77,62 @@ function buildFilter(filters?: { field: string; operator: string; value: unknown
|
|
|
67
77
|
* ```
|
|
68
78
|
*/
|
|
69
79
|
export function createPocketBaseDataProvider(options: PocketBaseProviderOptions): DataProvider {
|
|
70
|
-
const pb = options.pb
|
|
80
|
+
const pb = options.pb;
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
const transport: DataTransport = {
|
|
73
83
|
getApiUrl: () => pb.buildUrl('/api'),
|
|
74
84
|
|
|
75
|
-
async getList
|
|
85
|
+
async getList(params) {
|
|
76
86
|
const page = params.pagination?.current ?? 1;
|
|
77
87
|
const perPage = params.pagination?.pageSize ?? 10;
|
|
78
|
-
const result = await pb.collection(params.resource).getList(page, perPage, {
|
|
88
|
+
const result = await pb.collection(params.resource).getList(page, perPage, definedOptions({
|
|
79
89
|
sort: buildSort(params.sorters),
|
|
80
|
-
filter: buildFilter((params.filters || []).filter((f) => 'field' in f)
|
|
81
|
-
});
|
|
90
|
+
filter: buildFilter((params.filters || []).filter((f) => 'field' in f)),
|
|
91
|
+
}));
|
|
92
|
+
if (!isObject(result)) return result;
|
|
82
93
|
return {
|
|
83
|
-
data: result
|
|
84
|
-
total: result
|
|
94
|
+
data: result['items'],
|
|
95
|
+
total: result['totalItems'],
|
|
85
96
|
};
|
|
86
97
|
},
|
|
87
98
|
|
|
88
|
-
async getOne
|
|
99
|
+
async getOne(params) {
|
|
89
100
|
const record = await pb.collection(params.resource).getOne(String(params.id));
|
|
90
|
-
return { data: record
|
|
101
|
+
return { data: record };
|
|
91
102
|
},
|
|
92
103
|
|
|
93
|
-
async create
|
|
94
|
-
const
|
|
95
|
-
|
|
104
|
+
async create(params) {
|
|
105
|
+
const variables = inputRecord(params.variables);
|
|
106
|
+
const record = await pb.collection(params.resource).create(variables);
|
|
107
|
+
return { data: record };
|
|
96
108
|
},
|
|
97
109
|
|
|
98
|
-
async update
|
|
99
|
-
const
|
|
100
|
-
|
|
110
|
+
async update(params) {
|
|
111
|
+
const variables = inputRecord(params.variables);
|
|
112
|
+
const record = await pb.collection(params.resource).update(String(params.id), variables);
|
|
113
|
+
return { data: record };
|
|
101
114
|
},
|
|
102
115
|
|
|
103
|
-
async deleteOne
|
|
104
|
-
await pb.collection(params.resource).delete(String(params.id));
|
|
105
|
-
return { data: { id: params.id }
|
|
116
|
+
async deleteOne(params) {
|
|
117
|
+
const deleted = await pb.collection(params.resource).delete(String(params.id));
|
|
118
|
+
return deleted === true ? { data: { id: params.id } } : undefined;
|
|
106
119
|
},
|
|
107
120
|
|
|
108
|
-
async getMany
|
|
121
|
+
async getMany(params) {
|
|
109
122
|
const filter = params.ids.map(id => `id = '${id}'`).join(' || ');
|
|
110
123
|
const records = await pb.collection(params.resource).getFullList({ filter });
|
|
111
|
-
return { data: records
|
|
124
|
+
return { data: records };
|
|
112
125
|
},
|
|
113
126
|
|
|
114
|
-
async deleteMany
|
|
127
|
+
async deleteMany(params) {
|
|
115
128
|
const results: unknown[] = [];
|
|
116
129
|
for (const id of params.ids) {
|
|
117
|
-
await pb.collection(params.resource).delete(String(id));
|
|
130
|
+
const deleted = await pb.collection(params.resource).delete(String(id));
|
|
131
|
+
if (deleted !== true) return undefined;
|
|
118
132
|
results.push({ id });
|
|
119
133
|
}
|
|
120
|
-
return { data: results
|
|
134
|
+
return { data: results };
|
|
121
135
|
},
|
|
122
136
|
};
|
|
137
|
+
return withValidatedResponses(transport);
|
|
123
138
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
export { createPocketBaseDataProvider } from './data-provider';
|
|
4
4
|
export { createPocketBaseAuthProvider } from './auth-provider';
|
|
5
|
-
export {
|
|
5
|
+
export type { PocketBaseAuthClient, PocketBaseAuthOptions } from './auth-provider';
|
|
6
|
+
export { createPocketBaseLiveProvider, PocketBaseLiveError } from './live-provider';
|
|
7
|
+
export type { PocketBaseRealtimeClient, PocketBaseLiveOptions } from './live-provider';
|
package/src/live-provider.ts
CHANGED
|
@@ -1,65 +1,111 @@
|
|
|
1
|
-
|
|
1
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
2
2
|
import type { LiveProvider, LiveEvent } from '@svadmin/core';
|
|
3
|
+
import { collectionName, decode, nonempty } from './boundary';
|
|
3
4
|
|
|
4
|
-
interface
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export interface PocketBaseRealtimeClient {
|
|
6
|
+
collection(name: string): {
|
|
7
|
+
subscribe(topic: string, callback: (event: unknown) => void): Promise<unknown>;
|
|
8
|
+
};
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
type LiveErrorCode = 'INVALID_EVENT' | 'SUBSCRIBE_FAILED' | 'UNSUBSCRIBE_FAILED' | 'CALLBACK_FAILED';
|
|
12
|
+
|
|
13
|
+
export class PocketBaseLiveError extends Error {
|
|
14
|
+
constructor(readonly code: LiveErrorCode) {
|
|
15
|
+
super(`PocketBase realtime ${code.toLowerCase().replaceAll('_', ' ')}`);
|
|
16
|
+
this.name = 'PocketBaseLiveError';
|
|
17
|
+
}
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
export interface PocketBaseLiveOptions {
|
|
21
|
+
pb: PocketBaseRealtimeClient;
|
|
22
|
+
onError?: (error: PocketBaseLiveError) => void;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const eventSchema = Type.Object({
|
|
26
|
+
action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
|
|
27
|
+
record: Type.Object({ id: nonempty }),
|
|
28
|
+
}, { additionalProperties: false });
|
|
29
|
+
|
|
30
|
+
function mapAction(action: Static<typeof eventSchema>['action']): LiveEvent['type'] {
|
|
26
31
|
switch (action) {
|
|
27
32
|
case 'create': return 'INSERT';
|
|
28
33
|
case 'update': return 'UPDATE';
|
|
29
34
|
case 'delete': return 'DELETE';
|
|
30
|
-
default: return 'UPDATE';
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
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
|
-
*/
|
|
38
|
+
/** Each subscription owns the SDK-provided disposer, including cancellation during setup. */
|
|
46
39
|
export function createPocketBaseLiveProvider(options: PocketBaseLiveOptions): LiveProvider {
|
|
47
|
-
const
|
|
40
|
+
const report = (code: LiveErrorCode): void => {
|
|
41
|
+
const error = new PocketBaseLiveError(code);
|
|
42
|
+
if (!options.onError) {
|
|
43
|
+
console.error(error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
void Promise.resolve(options.onError(error)).catch(() => {
|
|
48
|
+
console.error('PocketBase realtime error handler failed');
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
console.error('PocketBase realtime error handler failed');
|
|
52
|
+
}
|
|
53
|
+
};
|
|
48
54
|
|
|
49
55
|
return {
|
|
50
56
|
subscribe({ resource, callback }) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const name = decode(collectionName, resource, 'input');
|
|
58
|
+
let active = true;
|
|
59
|
+
let dispose: (() => unknown) | undefined;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
const cleanup = (): void => {
|
|
62
|
+
if (!dispose || disposed) return;
|
|
63
|
+
disposed = true;
|
|
64
|
+
try {
|
|
65
|
+
const result = dispose();
|
|
66
|
+
void Promise.resolve(result).catch(() => report('UNSUBSCRIBE_FAILED'));
|
|
67
|
+
} catch {
|
|
68
|
+
report('UNSUBSCRIBE_FAILED');
|
|
69
|
+
}
|
|
57
70
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
const handler = (value: unknown): void => {
|
|
72
|
+
if (!active) return;
|
|
73
|
+
let event: LiveEvent;
|
|
74
|
+
try {
|
|
75
|
+
const data = decode(eventSchema, value, 'event');
|
|
76
|
+
event = { type: mapAction(data.action), resource: name, payload: data.record };
|
|
77
|
+
} catch {
|
|
78
|
+
report('INVALID_EVENT');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
void Promise.resolve(callback(event)).catch(() => report('CALLBACK_FAILED'));
|
|
83
|
+
} catch {
|
|
84
|
+
report('CALLBACK_FAILED');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const result = options.pb.collection(name).subscribe('*', handler);
|
|
89
|
+
void Promise.resolve(result).then((unsubscribe: unknown) => {
|
|
90
|
+
if (typeof unsubscribe !== 'function') {
|
|
91
|
+
active = false;
|
|
92
|
+
report('SUBSCRIBE_FAILED');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
dispose = (): unknown => unsubscribe();
|
|
96
|
+
if (!active) cleanup();
|
|
97
|
+
}, () => {
|
|
98
|
+
const wasActive = active;
|
|
99
|
+
active = false;
|
|
100
|
+
if (wasActive) report('SUBSCRIBE_FAILED');
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
active = false;
|
|
104
|
+
report('SUBSCRIBE_FAILED');
|
|
105
|
+
}
|
|
61
106
|
return () => {
|
|
62
|
-
|
|
107
|
+
active = false;
|
|
108
|
+
cleanup();
|
|
63
109
|
};
|
|
64
110
|
},
|
|
65
111
|
};
|