@picobase_app/client 0.5.2 → 1.0.2
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/dist/index.d.mts +211 -169
- package/dist/index.d.ts +211 -169
- package/dist/index.js +633 -349
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +634 -346
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -6
package/dist/index.mjs
CHANGED
|
@@ -1,130 +1,462 @@
|
|
|
1
|
-
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PicoBaseError = class extends Error {
|
|
3
|
+
constructor(message, code, status, details, fix) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.fix = fix;
|
|
9
|
+
this.name = "PicoBaseError";
|
|
10
|
+
}
|
|
11
|
+
/** Formatted error string including fix suggestion. */
|
|
12
|
+
toString() {
|
|
13
|
+
let s = `${this.name} [${this.code}]: ${this.message}`;
|
|
14
|
+
if (this.fix) s += `
|
|
15
|
+
Fix: ${this.fix}`;
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var InstanceUnavailableError = class extends PicoBaseError {
|
|
20
|
+
constructor(message = "Instance is not available. It may be stopped or starting up.") {
|
|
21
|
+
super(
|
|
22
|
+
message,
|
|
23
|
+
"INSTANCE_UNAVAILABLE",
|
|
24
|
+
503,
|
|
25
|
+
void 0,
|
|
26
|
+
"Check your instance status in the PicoBase dashboard, or wait a few seconds and retry. If this persists, your instance may have been stopped \u2014 restart it with `picobase status`."
|
|
27
|
+
);
|
|
28
|
+
this.name = "InstanceUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var AuthorizationError = class extends PicoBaseError {
|
|
32
|
+
constructor(message = "Invalid or missing API key.") {
|
|
33
|
+
super(
|
|
34
|
+
message,
|
|
35
|
+
"UNAUTHORIZED",
|
|
36
|
+
401,
|
|
37
|
+
void 0,
|
|
38
|
+
'Make sure PICOBASE_API_KEY is set in your .env file and matches a valid key from your dashboard. Keys start with "pbk_". You can generate a new key at https://picobase.com/dashboard.'
|
|
39
|
+
);
|
|
40
|
+
this.name = "AuthorizationError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var CollectionNotFoundError = class extends PicoBaseError {
|
|
44
|
+
constructor(collectionName) {
|
|
45
|
+
super(
|
|
46
|
+
`Collection "${collectionName}" not found.`,
|
|
47
|
+
"COLLECTION_NOT_FOUND",
|
|
48
|
+
404,
|
|
49
|
+
{ collection: collectionName },
|
|
50
|
+
`Make sure the collection "${collectionName}" exists in your PicoBase instance. Collections are auto-created when you first write data, or you can create them manually in the PicoBase dashboard under Collections.`
|
|
51
|
+
);
|
|
52
|
+
this.name = "CollectionNotFoundError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var RecordNotFoundError = class extends PicoBaseError {
|
|
56
|
+
constructor(collectionName, recordId) {
|
|
57
|
+
super(
|
|
58
|
+
`Record "${recordId}" not found in collection "${collectionName}".`,
|
|
59
|
+
"RECORD_NOT_FOUND",
|
|
60
|
+
404,
|
|
61
|
+
{ collection: collectionName, recordId },
|
|
62
|
+
'Check that the record ID is correct. IDs are 15-character alphanumeric strings (e.g., "abc123def456789").'
|
|
63
|
+
);
|
|
64
|
+
this.name = "RecordNotFoundError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var RequestError = class extends PicoBaseError {
|
|
68
|
+
constructor(message, status, details) {
|
|
69
|
+
const fix = requestErrorFix(status, message);
|
|
70
|
+
super(message, "REQUEST_FAILED", status, details, fix);
|
|
71
|
+
this.name = "RequestError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var ConfigurationError = class extends PicoBaseError {
|
|
75
|
+
constructor(message, fix) {
|
|
76
|
+
super(message, "CONFIGURATION_ERROR", void 0, void 0, fix);
|
|
77
|
+
this.name = "ConfigurationError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var RpcError = class extends PicoBaseError {
|
|
81
|
+
constructor(functionName, status, details) {
|
|
82
|
+
const fix = rpcErrorFix(functionName, status);
|
|
83
|
+
super(
|
|
84
|
+
`RPC function "${functionName}" failed.`,
|
|
85
|
+
"RPC_ERROR",
|
|
86
|
+
status,
|
|
87
|
+
details,
|
|
88
|
+
fix
|
|
89
|
+
);
|
|
90
|
+
this.name = "RpcError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function rpcErrorFix(functionName, status) {
|
|
94
|
+
if (status === 404) {
|
|
95
|
+
return `The RPC endpoint "/api/rpc/${functionName}" does not exist. Create a custom route in your PocketBase instance to handle this RPC call. See: https://pocketbase.io/docs/js-routing/`;
|
|
96
|
+
}
|
|
97
|
+
if (status === 400) {
|
|
98
|
+
return "Check the parameters you are passing to this RPC function. The function may be expecting different parameters or types.";
|
|
99
|
+
}
|
|
100
|
+
if (status === 403) {
|
|
101
|
+
return "You don't have permission to call this RPC function. Check the authentication requirements for this endpoint in your PocketBase routes.";
|
|
102
|
+
}
|
|
103
|
+
return "Check your PicoBase instance logs for details about this RPC error. Ensure the custom route is correctly implemented in your PocketBase setup.";
|
|
104
|
+
}
|
|
105
|
+
function requestErrorFix(status, message) {
|
|
106
|
+
switch (status) {
|
|
107
|
+
case 400:
|
|
108
|
+
return "Check the data you are sending \u2014 a required field may be missing or have the wrong type. Run `picobase typegen` to regenerate types and check your field names.";
|
|
109
|
+
case 403:
|
|
110
|
+
return "You don't have permission for this action. Check your collection API rules in the dashboard. By default, only authenticated users can read/write records.";
|
|
111
|
+
case 404:
|
|
112
|
+
if (message.toLowerCase().includes("collection"))
|
|
113
|
+
return "This collection does not exist yet. Write a record to auto-create it, or create it in the dashboard.";
|
|
114
|
+
return "The requested resource was not found. Double-check IDs and collection names.";
|
|
115
|
+
case 413:
|
|
116
|
+
return "The request payload is too large. Check file upload size limits in your instance settings.";
|
|
117
|
+
case 429:
|
|
118
|
+
return "Too many requests. Add a short delay between requests or implement client-side caching.";
|
|
119
|
+
default:
|
|
120
|
+
return "If this error persists, check your PicoBase dashboard for instance health and logs.";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
2
123
|
|
|
3
|
-
// src/
|
|
124
|
+
// src/http.ts
|
|
125
|
+
var PicoBaseHttpClient = class {
|
|
126
|
+
constructor(baseUrl, apiKey, options = {}) {
|
|
127
|
+
this.baseUrl = baseUrl;
|
|
128
|
+
this.apiKey = apiKey;
|
|
129
|
+
this.timeout = options.timeout ?? 3e4;
|
|
130
|
+
this.maxColdStartRetries = options.maxColdStartRetries ?? 3;
|
|
131
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
132
|
+
this.getToken = options.getToken;
|
|
133
|
+
}
|
|
134
|
+
async send(path, init = {}) {
|
|
135
|
+
const { query, body: rawBody, ...restInit } = init;
|
|
136
|
+
let url = `${this.baseUrl}${path}`;
|
|
137
|
+
if (query) {
|
|
138
|
+
const params = new URLSearchParams();
|
|
139
|
+
for (const [k, v] of Object.entries(query)) {
|
|
140
|
+
if (v !== void 0 && v !== null) params.append(k, String(v));
|
|
141
|
+
}
|
|
142
|
+
const qs = params.toString();
|
|
143
|
+
if (qs) url += `?${qs}`;
|
|
144
|
+
}
|
|
145
|
+
const headers = {
|
|
146
|
+
...restInit.headers ?? {}
|
|
147
|
+
};
|
|
148
|
+
headers["X-PicoBase-Key"] = this.apiKey;
|
|
149
|
+
const token = this.getToken?.();
|
|
150
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
151
|
+
let body;
|
|
152
|
+
if (rawBody instanceof FormData) {
|
|
153
|
+
body = rawBody;
|
|
154
|
+
} else if (rawBody !== void 0 && rawBody !== null) {
|
|
155
|
+
headers["Content-Type"] = "application/json";
|
|
156
|
+
body = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
|
|
157
|
+
}
|
|
158
|
+
let lastError = new Error("Unknown error");
|
|
159
|
+
for (let attempt = 0; attempt <= this.maxColdStartRetries; attempt++) {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
162
|
+
let response;
|
|
163
|
+
try {
|
|
164
|
+
response = await this.fetchImpl(url, {
|
|
165
|
+
...restInit,
|
|
166
|
+
headers,
|
|
167
|
+
body,
|
|
168
|
+
signal: controller.signal
|
|
169
|
+
});
|
|
170
|
+
} catch (fetchErr) {
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
173
|
+
throw new RequestError(msg, 0, fetchErr);
|
|
174
|
+
}
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
if (response.status === 204) return void 0;
|
|
178
|
+
return await response.json();
|
|
179
|
+
}
|
|
180
|
+
let errorData = null;
|
|
181
|
+
try {
|
|
182
|
+
errorData = await response.json();
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
const message = errorData?.message ?? errorData?.error ?? response.statusText;
|
|
186
|
+
const status = response.status;
|
|
187
|
+
if (status === 503) {
|
|
188
|
+
lastError = new RequestError(message, status, errorData);
|
|
189
|
+
if (attempt < this.maxColdStartRetries) {
|
|
190
|
+
const delay = Math.pow(2, attempt + 1) * 1e3;
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (status === 401) {
|
|
197
|
+
const code = errorData?.code;
|
|
198
|
+
if (!code || code === "INVALID_API_KEY") throw new AuthorizationError();
|
|
199
|
+
throw new AuthorizationError(message);
|
|
200
|
+
}
|
|
201
|
+
if (status === 404) {
|
|
202
|
+
const lower = message.toLowerCase();
|
|
203
|
+
if (lower.includes("collection") && (lower.includes("not found") || lower.includes("missing"))) {
|
|
204
|
+
const match = message.match(/["']([^"']+)["']/);
|
|
205
|
+
throw new CollectionNotFoundError(match?.[1] ?? "unknown");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new RequestError(message, status, errorData);
|
|
209
|
+
}
|
|
210
|
+
throw new InstanceUnavailableError(
|
|
211
|
+
`Instance unavailable after ${this.maxColdStartRetries} retries. Original error: ${lastError.message}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/auth-store.ts
|
|
217
|
+
var STORAGE_KEY_TOKEN = "picobase_token";
|
|
218
|
+
var STORAGE_KEY_RECORD = "picobase_record";
|
|
219
|
+
var AuthStore = class {
|
|
220
|
+
constructor() {
|
|
221
|
+
this._token = "";
|
|
222
|
+
this._record = null;
|
|
223
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
224
|
+
this._loadFromStorage();
|
|
225
|
+
}
|
|
226
|
+
/** Current JWT, or empty string if not signed in. */
|
|
227
|
+
get token() {
|
|
228
|
+
return this._token;
|
|
229
|
+
}
|
|
230
|
+
/** Currently signed-in user record, or null. */
|
|
231
|
+
get record() {
|
|
232
|
+
return this._record;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Returns true if a token is present and not yet expired.
|
|
236
|
+
* Checks JWT `exp` claim — no signature verification (the server does that).
|
|
237
|
+
*/
|
|
238
|
+
get isValid() {
|
|
239
|
+
if (!this._token) return false;
|
|
240
|
+
try {
|
|
241
|
+
const parts = this._token.split(".");
|
|
242
|
+
if (parts.length !== 3) return false;
|
|
243
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
244
|
+
const json = typeof atob !== "undefined" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
|
|
245
|
+
const payload = JSON.parse(json);
|
|
246
|
+
return typeof payload.exp === "number" && payload.exp * 1e3 > Date.now();
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Save a token (and optionally the user record). Persists to localStorage. */
|
|
252
|
+
save(token, record = null) {
|
|
253
|
+
this._token = token;
|
|
254
|
+
this._record = record;
|
|
255
|
+
try {
|
|
256
|
+
if (typeof localStorage !== "undefined") {
|
|
257
|
+
localStorage.setItem(STORAGE_KEY_TOKEN, token);
|
|
258
|
+
if (record) {
|
|
259
|
+
localStorage.setItem(STORAGE_KEY_RECORD, JSON.stringify(record));
|
|
260
|
+
} else {
|
|
261
|
+
localStorage.removeItem(STORAGE_KEY_RECORD);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
this._notify();
|
|
267
|
+
}
|
|
268
|
+
/** Clear the token and user record. Removes from localStorage. */
|
|
269
|
+
clear() {
|
|
270
|
+
this._token = "";
|
|
271
|
+
this._record = null;
|
|
272
|
+
try {
|
|
273
|
+
if (typeof localStorage !== "undefined") {
|
|
274
|
+
localStorage.removeItem(STORAGE_KEY_TOKEN);
|
|
275
|
+
localStorage.removeItem(STORAGE_KEY_RECORD);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
this._notify();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Register a listener for auth state changes.
|
|
283
|
+
* @returns Unsubscribe function.
|
|
284
|
+
*/
|
|
285
|
+
onChange(callback) {
|
|
286
|
+
this._listeners.add(callback);
|
|
287
|
+
return () => {
|
|
288
|
+
this._listeners.delete(callback);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
292
|
+
_loadFromStorage() {
|
|
293
|
+
try {
|
|
294
|
+
if (typeof localStorage !== "undefined") {
|
|
295
|
+
const token = localStorage.getItem(STORAGE_KEY_TOKEN);
|
|
296
|
+
if (token) {
|
|
297
|
+
this._token = token;
|
|
298
|
+
const raw = localStorage.getItem(STORAGE_KEY_RECORD);
|
|
299
|
+
if (raw) {
|
|
300
|
+
try {
|
|
301
|
+
this._record = JSON.parse(raw);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
_notify() {
|
|
311
|
+
for (const cb of this._listeners) {
|
|
312
|
+
try {
|
|
313
|
+
cb(this._token, this._record);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
4
319
|
|
|
5
320
|
// src/auth.ts
|
|
6
321
|
var PicoBaseAuth = class {
|
|
7
|
-
constructor(
|
|
322
|
+
constructor(http, authStore) {
|
|
8
323
|
this.listeners = /* @__PURE__ */ new Set();
|
|
9
|
-
this._collection = "
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
|
|
324
|
+
this._collection = "_auth_users";
|
|
325
|
+
this.http = http;
|
|
326
|
+
this.authStore = authStore;
|
|
327
|
+
this.authStore.onChange((token) => {
|
|
328
|
+
const record = this.authStore.record;
|
|
13
329
|
const event = token ? "SIGNED_IN" : "SIGNED_OUT";
|
|
14
330
|
this._notify(event, record);
|
|
15
331
|
});
|
|
16
332
|
}
|
|
17
333
|
/**
|
|
18
334
|
* Set which collection to authenticate against.
|
|
19
|
-
* Defaults to '
|
|
335
|
+
* Defaults to '_auth_users'. Use this if you have a custom auth collection.
|
|
20
336
|
*/
|
|
21
337
|
setCollection(name) {
|
|
22
338
|
this._collection = name;
|
|
23
339
|
return this;
|
|
24
340
|
}
|
|
25
341
|
/**
|
|
26
|
-
* Create a new user account.
|
|
342
|
+
* Create a new user account. Automatically signs the user in after creation.
|
|
27
343
|
*/
|
|
28
344
|
async signUp(options) {
|
|
29
345
|
const { email, password, passwordConfirm, ...rest } = options;
|
|
30
|
-
await this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
346
|
+
await this.http.send("/api/db/collections/_auth_users/records", {
|
|
347
|
+
method: "POST",
|
|
348
|
+
body: {
|
|
349
|
+
email,
|
|
350
|
+
password,
|
|
351
|
+
passwordConfirm: passwordConfirm ?? password,
|
|
352
|
+
...rest
|
|
353
|
+
}
|
|
35
354
|
});
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
token: authResult.token,
|
|
39
|
-
record: authResult.record
|
|
40
|
-
};
|
|
355
|
+
return this.signIn({ email, password });
|
|
41
356
|
}
|
|
42
357
|
/**
|
|
43
358
|
* Sign in with email and password.
|
|
44
359
|
*/
|
|
45
360
|
async signIn(options) {
|
|
46
|
-
const result = await this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
361
|
+
const result = await this.http.send(
|
|
362
|
+
`/api/db/collections/${this._collection}/auth-with-password`,
|
|
363
|
+
{
|
|
364
|
+
method: "POST",
|
|
365
|
+
body: { identity: options.email, password: options.password }
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
this.authStore.save(result.token, result.record);
|
|
369
|
+
return { token: result.token, record: result.record };
|
|
51
370
|
}
|
|
52
371
|
/**
|
|
53
372
|
* Sign in with an OAuth2 provider (Google, GitHub, etc.).
|
|
54
|
-
*
|
|
55
|
-
* In browser environments this opens a popup/redirect to the provider.
|
|
56
|
-
* Configure providers in your PicoBase dashboard.
|
|
57
373
|
*/
|
|
58
374
|
async signInWithOAuth(options) {
|
|
59
|
-
const result = await this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
375
|
+
const result = await this.http.send(
|
|
376
|
+
`/api/db/collections/${this._collection}/auth-with-oauth2`,
|
|
377
|
+
{
|
|
378
|
+
method: "POST",
|
|
379
|
+
body: {
|
|
380
|
+
provider: options.provider,
|
|
381
|
+
scopes: options.scopes,
|
|
382
|
+
createData: options.createData
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
this.authStore.save(result.token, result.record);
|
|
387
|
+
return { token: result.token, record: result.record };
|
|
69
388
|
}
|
|
70
389
|
/**
|
|
71
390
|
* Refresh the current auth token.
|
|
72
391
|
*/
|
|
73
392
|
async refreshToken() {
|
|
74
|
-
const result = await this.
|
|
393
|
+
const result = await this.http.send(
|
|
394
|
+
`/api/db/collections/${this._collection}/auth-refresh`,
|
|
395
|
+
{ method: "POST" }
|
|
396
|
+
);
|
|
397
|
+
this.authStore.save(result.token, result.record);
|
|
75
398
|
this._notify("TOKEN_REFRESHED", result.record);
|
|
76
|
-
return {
|
|
77
|
-
token: result.token,
|
|
78
|
-
record: result.record
|
|
79
|
-
};
|
|
399
|
+
return { token: result.token, record: result.record };
|
|
80
400
|
}
|
|
81
401
|
/**
|
|
82
402
|
* Send a password reset email.
|
|
83
403
|
*/
|
|
84
404
|
async requestPasswordReset(email) {
|
|
85
|
-
await this.
|
|
405
|
+
await this.http.send(
|
|
406
|
+
`/api/db/collections/${this._collection}/request-password-reset`,
|
|
407
|
+
{ method: "POST", body: { email } }
|
|
408
|
+
);
|
|
86
409
|
}
|
|
87
410
|
/**
|
|
88
411
|
* Confirm a password reset with the token from the reset email.
|
|
89
412
|
*/
|
|
90
413
|
async confirmPasswordReset(token, password, passwordConfirm) {
|
|
91
|
-
await this.
|
|
414
|
+
await this.http.send(
|
|
415
|
+
`/api/db/collections/${this._collection}/confirm-password-reset`,
|
|
416
|
+
{ method: "POST", body: { token, password, passwordConfirm: passwordConfirm ?? password } }
|
|
417
|
+
);
|
|
92
418
|
}
|
|
93
419
|
/**
|
|
94
420
|
* Send an email verification email.
|
|
95
421
|
*/
|
|
96
422
|
async requestVerification(email) {
|
|
97
|
-
await this.
|
|
423
|
+
await this.http.send(
|
|
424
|
+
`/api/db/collections/${this._collection}/request-verification`,
|
|
425
|
+
{ method: "POST", body: { email } }
|
|
426
|
+
);
|
|
98
427
|
}
|
|
99
428
|
/**
|
|
100
429
|
* Confirm email verification with the token from the verification email.
|
|
101
430
|
*/
|
|
102
431
|
async confirmVerification(token) {
|
|
103
|
-
await this.
|
|
432
|
+
await this.http.send(
|
|
433
|
+
`/api/db/collections/${this._collection}/confirm-verification`,
|
|
434
|
+
{ method: "POST", body: { token } }
|
|
435
|
+
);
|
|
104
436
|
}
|
|
105
437
|
/**
|
|
106
438
|
* Sign out the current user. Clears the local auth store.
|
|
107
439
|
*/
|
|
108
440
|
signOut() {
|
|
109
|
-
this.
|
|
441
|
+
this.authStore.clear();
|
|
110
442
|
}
|
|
111
443
|
/**
|
|
112
444
|
* Get the currently authenticated user record, or `null` if not signed in.
|
|
113
445
|
*/
|
|
114
446
|
get user() {
|
|
115
|
-
return this.
|
|
447
|
+
return this.authStore.record;
|
|
116
448
|
}
|
|
117
449
|
/**
|
|
118
450
|
* Get the current auth token, or empty string if not signed in.
|
|
119
451
|
*/
|
|
120
452
|
get token() {
|
|
121
|
-
return this.
|
|
453
|
+
return this.authStore.token;
|
|
122
454
|
}
|
|
123
455
|
/**
|
|
124
456
|
* Check if the current auth session is valid (token exists and not expired).
|
|
125
457
|
*/
|
|
126
458
|
get isValid() {
|
|
127
|
-
return this.
|
|
459
|
+
return this.authStore.isValid;
|
|
128
460
|
}
|
|
129
461
|
/**
|
|
130
462
|
* Listen to auth state changes. Returns an unsubscribe function.
|
|
@@ -132,9 +464,7 @@ var PicoBaseAuth = class {
|
|
|
132
464
|
* @example
|
|
133
465
|
* ```ts
|
|
134
466
|
* const unsubscribe = pb.auth.onStateChange((event, record) => {
|
|
135
|
-
* if (event === 'SIGNED_IN')
|
|
136
|
-
* console.log('Welcome', record.email)
|
|
137
|
-
* }
|
|
467
|
+
* if (event === 'SIGNED_IN') console.log('Welcome', record.email)
|
|
138
468
|
* })
|
|
139
469
|
*
|
|
140
470
|
* // Later:
|
|
@@ -159,9 +489,10 @@ var PicoBaseAuth = class {
|
|
|
159
489
|
|
|
160
490
|
// src/collection.ts
|
|
161
491
|
var PicoBaseCollection = class {
|
|
162
|
-
constructor(
|
|
163
|
-
this.
|
|
492
|
+
constructor(http, name, realtime) {
|
|
493
|
+
this.http = http;
|
|
164
494
|
this.name = name;
|
|
495
|
+
this.rt = realtime;
|
|
165
496
|
}
|
|
166
497
|
/**
|
|
167
498
|
* Fetch a paginated list of records.
|
|
@@ -171,7 +502,17 @@ var PicoBaseCollection = class {
|
|
|
171
502
|
* @param options - Filter, sort, expand, fields.
|
|
172
503
|
*/
|
|
173
504
|
async getList(page = 1, perPage = 30, options = {}) {
|
|
174
|
-
|
|
505
|
+
const { sort, filter, expand, fields, skipTotal, ...rest } = options;
|
|
506
|
+
const query = { page, perPage, ...rest };
|
|
507
|
+
if (sort) query.sort = sort;
|
|
508
|
+
if (filter) query.filter = filter;
|
|
509
|
+
if (expand) query.expand = expand;
|
|
510
|
+
if (fields) query.fields = fields;
|
|
511
|
+
if (skipTotal) query.skipTotal = "1";
|
|
512
|
+
return this.http.send(
|
|
513
|
+
`/api/db/collections/${this.name}/records`,
|
|
514
|
+
{ method: "GET", query }
|
|
515
|
+
);
|
|
175
516
|
}
|
|
176
517
|
/**
|
|
177
518
|
* Fetch all records matching the filter (auto-paginates).
|
|
@@ -179,13 +520,28 @@ var PicoBaseCollection = class {
|
|
|
179
520
|
* **Warning:** Use with caution on large collections. Prefer `getList()` with pagination.
|
|
180
521
|
*/
|
|
181
522
|
async getFullList(options = {}) {
|
|
182
|
-
|
|
523
|
+
const all = [];
|
|
524
|
+
let page = 1;
|
|
525
|
+
const perPage = 200;
|
|
526
|
+
while (true) {
|
|
527
|
+
const result = await this.getList(page, perPage, options);
|
|
528
|
+
all.push(...result.items);
|
|
529
|
+
if (page >= result.totalPages) break;
|
|
530
|
+
page++;
|
|
531
|
+
}
|
|
532
|
+
return all;
|
|
183
533
|
}
|
|
184
534
|
/**
|
|
185
535
|
* Fetch a single record by ID.
|
|
186
536
|
*/
|
|
187
537
|
async getOne(id, options = {}) {
|
|
188
|
-
|
|
538
|
+
const query = {};
|
|
539
|
+
if (options.expand) query.expand = options.expand;
|
|
540
|
+
if (options.fields) query.fields = options.fields;
|
|
541
|
+
return this.http.send(
|
|
542
|
+
`/api/db/collections/${this.name}/records/${id}`,
|
|
543
|
+
{ method: "GET", query: Object.keys(query).length ? query : void 0 }
|
|
544
|
+
);
|
|
189
545
|
}
|
|
190
546
|
/**
|
|
191
547
|
* Fetch the first record matching a filter.
|
|
@@ -196,7 +552,11 @@ var PicoBaseCollection = class {
|
|
|
196
552
|
* ```
|
|
197
553
|
*/
|
|
198
554
|
async getFirstListItem(filter, options = {}) {
|
|
199
|
-
|
|
555
|
+
const result = await this.getList(1, 1, { filter, ...options });
|
|
556
|
+
if (!result.items.length) {
|
|
557
|
+
throw new RecordNotFoundError(this.name, filter);
|
|
558
|
+
}
|
|
559
|
+
return result.items[0];
|
|
200
560
|
}
|
|
201
561
|
/**
|
|
202
562
|
* Create a new record.
|
|
@@ -204,7 +564,17 @@ var PicoBaseCollection = class {
|
|
|
204
564
|
* @param data - Record data. Can be a plain object or `FormData` (for file uploads).
|
|
205
565
|
*/
|
|
206
566
|
async create(data, options = {}) {
|
|
207
|
-
|
|
567
|
+
const query = {};
|
|
568
|
+
if (options.expand) query.expand = options.expand;
|
|
569
|
+
if (options.fields) query.fields = options.fields;
|
|
570
|
+
return this.http.send(
|
|
571
|
+
`/api/db/collections/${this.name}/records`,
|
|
572
|
+
{
|
|
573
|
+
method: "POST",
|
|
574
|
+
body: data,
|
|
575
|
+
query: Object.keys(query).length ? query : void 0
|
|
576
|
+
}
|
|
577
|
+
);
|
|
208
578
|
}
|
|
209
579
|
/**
|
|
210
580
|
* Update an existing record.
|
|
@@ -213,19 +583,33 @@ var PicoBaseCollection = class {
|
|
|
213
583
|
* @param data - Fields to update. Can be a plain object or `FormData`.
|
|
214
584
|
*/
|
|
215
585
|
async update(id, data, options = {}) {
|
|
216
|
-
|
|
586
|
+
const query = {};
|
|
587
|
+
if (options.expand) query.expand = options.expand;
|
|
588
|
+
if (options.fields) query.fields = options.fields;
|
|
589
|
+
return this.http.send(
|
|
590
|
+
`/api/db/collections/${this.name}/records/${id}`,
|
|
591
|
+
{
|
|
592
|
+
method: "PATCH",
|
|
593
|
+
body: data,
|
|
594
|
+
query: Object.keys(query).length ? query : void 0
|
|
595
|
+
}
|
|
596
|
+
);
|
|
217
597
|
}
|
|
218
598
|
/**
|
|
219
599
|
* Delete a record by ID.
|
|
220
600
|
*/
|
|
221
601
|
async delete(id) {
|
|
222
|
-
|
|
602
|
+
await this.http.send(
|
|
603
|
+
`/api/db/collections/${this.name}/records/${id}`,
|
|
604
|
+
{ method: "DELETE" }
|
|
605
|
+
);
|
|
606
|
+
return true;
|
|
223
607
|
}
|
|
224
608
|
/**
|
|
225
609
|
* Subscribe to realtime changes on this collection.
|
|
226
610
|
*
|
|
227
611
|
* @param callback - Called on every create/update/delete event.
|
|
228
|
-
* @param filter - Optional: only receive events matching this filter.
|
|
612
|
+
* @param filter - Optional: only receive events matching this filter (client-side).
|
|
229
613
|
* @returns Unsubscribe function.
|
|
230
614
|
*
|
|
231
615
|
* @example
|
|
@@ -239,9 +623,7 @@ var PicoBaseCollection = class {
|
|
|
239
623
|
* ```
|
|
240
624
|
*/
|
|
241
625
|
async subscribe(callback, filter) {
|
|
242
|
-
|
|
243
|
-
await this.pb.collection(this.name).subscribe(topic, callback, filter ? { filter } : void 0);
|
|
244
|
-
return () => this.pb.collection(this.name).unsubscribe(topic);
|
|
626
|
+
return this.rt.subscribe(this.name, callback);
|
|
245
627
|
}
|
|
246
628
|
/**
|
|
247
629
|
* Subscribe to changes on a specific record.
|
|
@@ -251,15 +633,20 @@ var PicoBaseCollection = class {
|
|
|
251
633
|
* @returns Unsubscribe function.
|
|
252
634
|
*/
|
|
253
635
|
async subscribeOne(id, callback) {
|
|
254
|
-
|
|
255
|
-
return () => this.pb.collection(this.name).unsubscribe(id);
|
|
636
|
+
return this.rt.subscribeRecord(this.name, id, callback);
|
|
256
637
|
}
|
|
257
638
|
};
|
|
258
639
|
|
|
259
640
|
// src/realtime.ts
|
|
260
641
|
var PicoBaseRealtime = class {
|
|
261
|
-
constructor(
|
|
262
|
-
this.
|
|
642
|
+
constructor(baseUrl, apiKey, getToken) {
|
|
643
|
+
this.eventSource = null;
|
|
644
|
+
// key = collection or collection/recordId
|
|
645
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
646
|
+
this.counter = 0;
|
|
647
|
+
this.baseUrl = baseUrl;
|
|
648
|
+
this.apiKey = apiKey;
|
|
649
|
+
this.getToken = getToken;
|
|
263
650
|
}
|
|
264
651
|
/**
|
|
265
652
|
* Subscribe to realtime events on a collection.
|
|
@@ -269,8 +656,11 @@ var PicoBaseRealtime = class {
|
|
|
269
656
|
* @returns Unsubscribe function.
|
|
270
657
|
*/
|
|
271
658
|
async subscribe(collection, callback) {
|
|
272
|
-
|
|
273
|
-
|
|
659
|
+
const id = this._addSub({ collection, callback });
|
|
660
|
+
this._ensureConnection();
|
|
661
|
+
return async () => {
|
|
662
|
+
this._removeSub(id);
|
|
663
|
+
};
|
|
274
664
|
}
|
|
275
665
|
/**
|
|
276
666
|
* Subscribe to realtime events on a specific record.
|
|
@@ -281,242 +671,217 @@ var PicoBaseRealtime = class {
|
|
|
281
671
|
* @returns Unsubscribe function.
|
|
282
672
|
*/
|
|
283
673
|
async subscribeRecord(collection, recordId, callback) {
|
|
284
|
-
|
|
285
|
-
|
|
674
|
+
const id = this._addSub({ collection, recordId, callback });
|
|
675
|
+
this._ensureConnection();
|
|
676
|
+
return async () => {
|
|
677
|
+
this._removeSub(id);
|
|
678
|
+
};
|
|
286
679
|
}
|
|
287
680
|
/**
|
|
288
681
|
* Unsubscribe from all realtime events on a collection.
|
|
289
682
|
*/
|
|
290
683
|
async unsubscribe(collection) {
|
|
291
|
-
|
|
684
|
+
this.subs.delete(collection);
|
|
685
|
+
this._pruneIfEmpty();
|
|
292
686
|
}
|
|
293
687
|
/**
|
|
294
|
-
* Unsubscribe from ALL realtime events
|
|
295
|
-
* automatically closed when there are no remaining subscriptions.
|
|
688
|
+
* Unsubscribe from ALL realtime events and close the SSE connection.
|
|
296
689
|
*/
|
|
297
690
|
async disconnectAll() {
|
|
298
|
-
|
|
691
|
+
this.subs.clear();
|
|
692
|
+
this._closeConnection();
|
|
693
|
+
}
|
|
694
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
695
|
+
_addSub(sub) {
|
|
696
|
+
const id = `sub_${++this.counter}`;
|
|
697
|
+
const key = sub.recordId ? `${sub.collection}/${sub.recordId}` : sub.collection;
|
|
698
|
+
if (!this.subs.has(key)) this.subs.set(key, /* @__PURE__ */ new Set());
|
|
699
|
+
this.subs.get(key).add({ ...sub, _id: id });
|
|
700
|
+
return id;
|
|
701
|
+
}
|
|
702
|
+
_removeSub(id) {
|
|
703
|
+
for (const [key, set] of this.subs) {
|
|
704
|
+
for (const sub of set) {
|
|
705
|
+
if (sub._id === id) {
|
|
706
|
+
set.delete(sub);
|
|
707
|
+
if (set.size === 0) this.subs.delete(key);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this._pruneIfEmpty();
|
|
713
|
+
}
|
|
714
|
+
_pruneIfEmpty() {
|
|
715
|
+
if (this.subs.size === 0) this._closeConnection();
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Open SSE connection to `/api/db/realtime`.
|
|
719
|
+
* No-ops in environments where EventSource is unavailable (SSR / Node).
|
|
720
|
+
*/
|
|
721
|
+
_ensureConnection() {
|
|
722
|
+
if (this.eventSource) return;
|
|
723
|
+
if (typeof EventSource === "undefined") return;
|
|
724
|
+
const url = new URL(`${this.baseUrl}/api/db/realtime`);
|
|
725
|
+
url.searchParams.set("apiKey", this.apiKey);
|
|
726
|
+
const token = this.getToken();
|
|
727
|
+
if (token) url.searchParams.set("token", token);
|
|
728
|
+
const es = new EventSource(url.toString());
|
|
729
|
+
this.eventSource = es;
|
|
730
|
+
es.onmessage = (event) => {
|
|
731
|
+
try {
|
|
732
|
+
const data = JSON.parse(event.data);
|
|
733
|
+
this._dispatch(data);
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
for (const action of ["create", "update", "delete"]) {
|
|
738
|
+
es.addEventListener(action, (event) => {
|
|
739
|
+
try {
|
|
740
|
+
const data = JSON.parse(event.data);
|
|
741
|
+
this._dispatch(data);
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
es.onerror = () => {
|
|
747
|
+
if (this.subs.size === 0) this._closeConnection();
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
_closeConnection() {
|
|
751
|
+
if (this.eventSource) {
|
|
752
|
+
this.eventSource.close();
|
|
753
|
+
this.eventSource = null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
_dispatch(event) {
|
|
757
|
+
const payload = {
|
|
758
|
+
action: event.action,
|
|
759
|
+
record: event.record
|
|
760
|
+
};
|
|
761
|
+
const collSubs = this.subs.get(event.collection);
|
|
762
|
+
if (collSubs) {
|
|
763
|
+
for (const sub of collSubs) {
|
|
764
|
+
try {
|
|
765
|
+
sub.callback(payload);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const recordKey = `${event.collection}/${event.record?.id}`;
|
|
771
|
+
const recSubs = this.subs.get(recordKey);
|
|
772
|
+
if (recSubs) {
|
|
773
|
+
for (const sub of recSubs) {
|
|
774
|
+
try {
|
|
775
|
+
sub.callback(payload);
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
299
780
|
}
|
|
300
781
|
};
|
|
301
782
|
|
|
302
783
|
// src/storage.ts
|
|
303
784
|
var PicoBaseStorage = class {
|
|
304
|
-
constructor(
|
|
305
|
-
this.
|
|
785
|
+
constructor(baseUrl, http) {
|
|
786
|
+
this.baseUrl = baseUrl;
|
|
787
|
+
this.http = http;
|
|
306
788
|
}
|
|
307
789
|
/**
|
|
308
|
-
* Get the
|
|
790
|
+
* Get the URL for a file attached to a record.
|
|
309
791
|
*
|
|
310
|
-
* @param record - The record that owns the file.
|
|
792
|
+
* @param record - The record that owns the file. Must have `collectionName` and `id`.
|
|
311
793
|
* @param filename - The filename (as stored in the record's file field).
|
|
312
794
|
* @param options - Optional: thumb size, token for protected files, download flag.
|
|
313
795
|
*/
|
|
314
796
|
getFileUrl(record, filename, options = {}) {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (options.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return this.pb.files.getURL(record, filename, queryParams);
|
|
797
|
+
const collection = record.collectionName ?? "";
|
|
798
|
+
const recordId = record.id;
|
|
799
|
+
let url = `${this.baseUrl}/api/db/files/${collection}/${recordId}/${filename}`;
|
|
800
|
+
const params = new URLSearchParams();
|
|
801
|
+
if (options.thumb) params.set("thumb", options.thumb);
|
|
802
|
+
if (options.token) params.set("token", options.token);
|
|
803
|
+
if (options.download) params.set("download", "1");
|
|
804
|
+
const qs = params.toString();
|
|
805
|
+
if (qs) url += `?${qs}`;
|
|
806
|
+
return url;
|
|
326
807
|
}
|
|
327
808
|
/**
|
|
328
|
-
* Generate a temporary file access token.
|
|
329
|
-
*
|
|
330
|
-
* Use this for accessing protected files. Tokens are short-lived.
|
|
809
|
+
* Generate a temporary file access token for protected files.
|
|
810
|
+
* Tokens are short-lived. Available after Phase 7 ships.
|
|
331
811
|
*/
|
|
332
812
|
async getFileToken() {
|
|
333
|
-
|
|
813
|
+
const result = await this.http.send(
|
|
814
|
+
"/api/db/files/token",
|
|
815
|
+
{ method: "POST" }
|
|
816
|
+
);
|
|
817
|
+
return result.token;
|
|
334
818
|
}
|
|
335
819
|
};
|
|
336
820
|
|
|
337
821
|
// src/admin.ts
|
|
338
822
|
var PicoBaseAdmin = class {
|
|
339
|
-
constructor(
|
|
340
|
-
this.
|
|
823
|
+
constructor(http) {
|
|
824
|
+
this.http = http;
|
|
341
825
|
}
|
|
342
826
|
/**
|
|
343
827
|
* Fetch a list of all collections.
|
|
344
828
|
*/
|
|
345
829
|
async listCollections() {
|
|
346
|
-
const result = await this.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
830
|
+
const result = await this.http.send(
|
|
831
|
+
"/api/db/collections",
|
|
832
|
+
{ method: "GET" }
|
|
833
|
+
);
|
|
834
|
+
return result.items ?? [];
|
|
350
835
|
}
|
|
351
836
|
/**
|
|
352
837
|
* Fetch a single collection by ID or name.
|
|
353
838
|
*/
|
|
354
839
|
async getCollection(idOrName) {
|
|
355
|
-
return this.
|
|
356
|
-
|
|
357
|
-
|
|
840
|
+
return this.http.send(
|
|
841
|
+
`/api/db/collections/${idOrName}`,
|
|
842
|
+
{ method: "GET" }
|
|
843
|
+
);
|
|
358
844
|
}
|
|
359
845
|
/**
|
|
360
846
|
* Create a new collection.
|
|
847
|
+
*
|
|
848
|
+
* @param data.type - `"flexible"` (JSONB, schema-free) or `"strict"` (typed SQL columns).
|
|
361
849
|
*/
|
|
362
850
|
async createCollection(data) {
|
|
363
|
-
return this.
|
|
364
|
-
|
|
365
|
-
body: data
|
|
366
|
-
|
|
851
|
+
return this.http.send(
|
|
852
|
+
"/api/db/collections",
|
|
853
|
+
{ method: "POST", body: data }
|
|
854
|
+
);
|
|
367
855
|
}
|
|
368
856
|
/**
|
|
369
857
|
* Update an existing collection.
|
|
370
858
|
*/
|
|
371
859
|
async updateCollection(idOrName, data) {
|
|
372
|
-
return this.
|
|
373
|
-
|
|
374
|
-
body: data
|
|
375
|
-
|
|
860
|
+
return this.http.send(
|
|
861
|
+
`/api/db/collections/${idOrName}`,
|
|
862
|
+
{ method: "PATCH", body: data }
|
|
863
|
+
);
|
|
376
864
|
}
|
|
377
865
|
/**
|
|
378
866
|
* Delete a collection.
|
|
379
867
|
*/
|
|
380
868
|
async deleteCollection(idOrName) {
|
|
381
869
|
try {
|
|
382
|
-
await this.
|
|
383
|
-
|
|
384
|
-
|
|
870
|
+
await this.http.send(
|
|
871
|
+
`/api/db/collections/${idOrName}`,
|
|
872
|
+
{ method: "DELETE" }
|
|
873
|
+
);
|
|
385
874
|
return true;
|
|
386
|
-
} catch
|
|
875
|
+
} catch {
|
|
387
876
|
return false;
|
|
388
877
|
}
|
|
389
878
|
}
|
|
390
879
|
};
|
|
391
880
|
|
|
392
|
-
// src/errors.ts
|
|
393
|
-
var PicoBaseError = class extends Error {
|
|
394
|
-
constructor(message, code, status, details, fix) {
|
|
395
|
-
super(message);
|
|
396
|
-
this.code = code;
|
|
397
|
-
this.status = status;
|
|
398
|
-
this.details = details;
|
|
399
|
-
this.fix = fix;
|
|
400
|
-
this.name = "PicoBaseError";
|
|
401
|
-
}
|
|
402
|
-
/** Formatted error string including fix suggestion. */
|
|
403
|
-
toString() {
|
|
404
|
-
let s = `${this.name} [${this.code}]: ${this.message}`;
|
|
405
|
-
if (this.fix) s += `
|
|
406
|
-
Fix: ${this.fix}`;
|
|
407
|
-
return s;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
var InstanceUnavailableError = class extends PicoBaseError {
|
|
411
|
-
constructor(message = "Instance is not available. It may be stopped or starting up.") {
|
|
412
|
-
super(
|
|
413
|
-
message,
|
|
414
|
-
"INSTANCE_UNAVAILABLE",
|
|
415
|
-
503,
|
|
416
|
-
void 0,
|
|
417
|
-
"Check your instance status in the PicoBase dashboard, or wait a few seconds and retry. If this persists, your instance may have been stopped \u2014 restart it with `picobase status`."
|
|
418
|
-
);
|
|
419
|
-
this.name = "InstanceUnavailableError";
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
var AuthorizationError = class extends PicoBaseError {
|
|
423
|
-
constructor(message = "Invalid or missing API key.") {
|
|
424
|
-
super(
|
|
425
|
-
message,
|
|
426
|
-
"UNAUTHORIZED",
|
|
427
|
-
401,
|
|
428
|
-
void 0,
|
|
429
|
-
'Make sure PICOBASE_API_KEY is set in your .env file and matches a valid key from your dashboard. Keys start with "pbk_". You can generate a new key at https://picobase.com/dashboard.'
|
|
430
|
-
);
|
|
431
|
-
this.name = "AuthorizationError";
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
var CollectionNotFoundError = class extends PicoBaseError {
|
|
435
|
-
constructor(collectionName) {
|
|
436
|
-
super(
|
|
437
|
-
`Collection "${collectionName}" not found.`,
|
|
438
|
-
"COLLECTION_NOT_FOUND",
|
|
439
|
-
404,
|
|
440
|
-
{ collection: collectionName },
|
|
441
|
-
`Make sure the collection "${collectionName}" exists in your PicoBase instance. Collections are auto-created when you first write data, or you can create them manually in the PicoBase dashboard under Collections.`
|
|
442
|
-
);
|
|
443
|
-
this.name = "CollectionNotFoundError";
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
var RecordNotFoundError = class extends PicoBaseError {
|
|
447
|
-
constructor(collectionName, recordId) {
|
|
448
|
-
super(
|
|
449
|
-
`Record "${recordId}" not found in collection "${collectionName}".`,
|
|
450
|
-
"RECORD_NOT_FOUND",
|
|
451
|
-
404,
|
|
452
|
-
{ collection: collectionName, recordId },
|
|
453
|
-
'Check that the record ID is correct. IDs are 15-character alphanumeric strings (e.g., "abc123def456789").'
|
|
454
|
-
);
|
|
455
|
-
this.name = "RecordNotFoundError";
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
var RequestError = class extends PicoBaseError {
|
|
459
|
-
constructor(message, status, details) {
|
|
460
|
-
const fix = requestErrorFix(status, message);
|
|
461
|
-
super(message, "REQUEST_FAILED", status, details, fix);
|
|
462
|
-
this.name = "RequestError";
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
var ConfigurationError = class extends PicoBaseError {
|
|
466
|
-
constructor(message, fix) {
|
|
467
|
-
super(message, "CONFIGURATION_ERROR", void 0, void 0, fix);
|
|
468
|
-
this.name = "ConfigurationError";
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
var RpcError = class extends PicoBaseError {
|
|
472
|
-
constructor(functionName, status, details) {
|
|
473
|
-
const fix = rpcErrorFix(functionName, status);
|
|
474
|
-
super(
|
|
475
|
-
`RPC function "${functionName}" failed.`,
|
|
476
|
-
"RPC_ERROR",
|
|
477
|
-
status,
|
|
478
|
-
details,
|
|
479
|
-
fix
|
|
480
|
-
);
|
|
481
|
-
this.name = "RpcError";
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
function rpcErrorFix(functionName, status) {
|
|
485
|
-
if (status === 404) {
|
|
486
|
-
return `The RPC endpoint "/api/rpc/${functionName}" does not exist. Create a custom route in your PocketBase instance to handle this RPC call. See: https://pocketbase.io/docs/js-routing/`;
|
|
487
|
-
}
|
|
488
|
-
if (status === 400) {
|
|
489
|
-
return "Check the parameters you are passing to this RPC function. The function may be expecting different parameters or types.";
|
|
490
|
-
}
|
|
491
|
-
if (status === 403) {
|
|
492
|
-
return "You don't have permission to call this RPC function. Check the authentication requirements for this endpoint in your PocketBase routes.";
|
|
493
|
-
}
|
|
494
|
-
return "Check your PicoBase instance logs for details about this RPC error. Ensure the custom route is correctly implemented in your PocketBase setup.";
|
|
495
|
-
}
|
|
496
|
-
function requestErrorFix(status, message) {
|
|
497
|
-
switch (status) {
|
|
498
|
-
case 400:
|
|
499
|
-
return "Check the data you are sending \u2014 a required field may be missing or have the wrong type. Run `picobase typegen` to regenerate types and check your field names.";
|
|
500
|
-
case 403:
|
|
501
|
-
return "You don't have permission for this action. Check your collection API rules in the dashboard. By default, only authenticated users can read/write records.";
|
|
502
|
-
case 404:
|
|
503
|
-
if (message.toLowerCase().includes("collection"))
|
|
504
|
-
return "This collection does not exist yet. Write a record to auto-create it, or create it in the dashboard.";
|
|
505
|
-
return "The requested resource was not found. Double-check IDs and collection names.";
|
|
506
|
-
case 413:
|
|
507
|
-
return "The request payload is too large. Check file upload size limits in your instance settings.";
|
|
508
|
-
case 429:
|
|
509
|
-
return "Too many requests. Add a short delay between requests or implement client-side caching.";
|
|
510
|
-
default:
|
|
511
|
-
return "If this error persists, check your PicoBase dashboard for instance health and logs.";
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
881
|
// src/client.ts
|
|
516
882
|
var DEFAULT_OPTIONS = {
|
|
517
883
|
timeout: 3e4,
|
|
518
|
-
maxColdStartRetries: 3
|
|
519
|
-
lang: "en-US"
|
|
884
|
+
maxColdStartRetries: 3
|
|
520
885
|
};
|
|
521
886
|
var PicoBaseClient = class {
|
|
522
887
|
constructor(url, apiKey, options = {}) {
|
|
@@ -538,25 +903,19 @@ var PicoBaseClient = class {
|
|
|
538
903
|
`Use the full URL: createClient("https://${url}", "...")`
|
|
539
904
|
);
|
|
540
905
|
}
|
|
541
|
-
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
this.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
};
|
|
555
|
-
this._wrapSendWithRetry();
|
|
556
|
-
this.auth = new PicoBaseAuth(this.pb);
|
|
557
|
-
this.realtime = new PicoBaseRealtime(this.pb);
|
|
558
|
-
this.storage = new PicoBaseStorage(this.pb);
|
|
559
|
-
this.admin = new PicoBaseAdmin(this.pb);
|
|
906
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
907
|
+
this.baseUrl = url.replace(/\/+$/, "");
|
|
908
|
+
this._authStore = new AuthStore();
|
|
909
|
+
this._http = new PicoBaseHttpClient(this.baseUrl, apiKey, {
|
|
910
|
+
timeout: mergedOptions.timeout,
|
|
911
|
+
maxColdStartRetries: mergedOptions.maxColdStartRetries,
|
|
912
|
+
fetch: options.fetch,
|
|
913
|
+
getToken: () => this._authStore.token
|
|
914
|
+
});
|
|
915
|
+
this.auth = new PicoBaseAuth(this._http, this._authStore);
|
|
916
|
+
this.realtime = new PicoBaseRealtime(this.baseUrl, apiKey, () => this._authStore.token);
|
|
917
|
+
this.storage = new PicoBaseStorage(this.baseUrl, this._http);
|
|
918
|
+
this.admin = new PicoBaseAdmin(this._http);
|
|
560
919
|
}
|
|
561
920
|
/**
|
|
562
921
|
* Access a collection for CRUD operations.
|
|
@@ -567,40 +926,23 @@ var PicoBaseClient = class {
|
|
|
567
926
|
* ```
|
|
568
927
|
*/
|
|
569
928
|
collection(name) {
|
|
570
|
-
return new PicoBaseCollection(this.
|
|
929
|
+
return new PicoBaseCollection(this._http, name, this.realtime);
|
|
571
930
|
}
|
|
572
931
|
/**
|
|
573
|
-
*
|
|
574
|
-
* Proxies to PocketBase's send() method.
|
|
932
|
+
* Send a raw HTTP request to the PicoBase API.
|
|
575
933
|
*/
|
|
576
934
|
async send(path, options) {
|
|
577
|
-
return this.
|
|
935
|
+
return this._http.send(path, options);
|
|
578
936
|
}
|
|
579
937
|
/**
|
|
580
|
-
* Call a remote procedure (RPC)
|
|
938
|
+
* Call a remote procedure (RPC) — convenience wrapper for custom API endpoints.
|
|
581
939
|
*
|
|
582
|
-
* Maps Supabase-style RPC calls
|
|
583
|
-
* - `pb.rpc('my_function', params)` → `POST /api/rpc/my_function` with params as body
|
|
940
|
+
* Maps Supabase-style RPC calls: `pb.rpc('my_fn', params)` → `POST /api/rpc/my_fn`
|
|
584
941
|
*
|
|
585
942
|
* @example
|
|
586
943
|
* ```ts
|
|
587
|
-
* // Simple RPC call
|
|
588
944
|
* const result = await pb.rpc('calculate_total', { cart_id: '123' })
|
|
589
|
-
*
|
|
590
|
-
* // Complex RPC with typed response
|
|
591
|
-
* interface DashboardStats {
|
|
592
|
-
* posts: number
|
|
593
|
-
* comments: number
|
|
594
|
-
* followers: number
|
|
595
|
-
* }
|
|
596
|
-
* const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
|
|
597
|
-
* user_id: currentUser.id
|
|
598
|
-
* })
|
|
599
945
|
* ```
|
|
600
|
-
*
|
|
601
|
-
* @param functionName The name of the RPC function to call
|
|
602
|
-
* @param params Optional parameters to pass to the function
|
|
603
|
-
* @returns The function result
|
|
604
946
|
*/
|
|
605
947
|
async rpc(functionName, params) {
|
|
606
948
|
try {
|
|
@@ -610,29 +952,19 @@ var PicoBaseClient = class {
|
|
|
610
952
|
});
|
|
611
953
|
} catch (err) {
|
|
612
954
|
const status = err?.status ?? 500;
|
|
613
|
-
const details = err?.
|
|
955
|
+
const details = err?.details;
|
|
614
956
|
throw new RpcError(functionName, status, details);
|
|
615
957
|
}
|
|
616
958
|
}
|
|
617
959
|
/**
|
|
618
|
-
* Proactively
|
|
619
|
-
*
|
|
620
|
-
* to
|
|
621
|
-
*
|
|
622
|
-
* This method guarantees that the instance is fully running and able to serve requests
|
|
623
|
-
* when the promise resolves.
|
|
624
|
-
*
|
|
625
|
-
* @example
|
|
626
|
-
* ```ts
|
|
627
|
-
* // In a useEffect on your login page:
|
|
628
|
-
* useEffect(() => {
|
|
629
|
-
* pb.wake().catch(console.error)
|
|
630
|
-
* }, [])
|
|
631
|
-
* ```
|
|
960
|
+
* Proactively warm up the Neon connection after scale-to-zero.
|
|
961
|
+
*
|
|
962
|
+
* Useful to call when a user navigates to a login page or before critical
|
|
963
|
+
* operations, to absorb cold-start latency upfront.
|
|
632
964
|
*/
|
|
633
965
|
async wake() {
|
|
634
966
|
try {
|
|
635
|
-
await this.send("/api/health", { method: "GET" });
|
|
967
|
+
await this.send("/api/db/health", { method: "GET" });
|
|
636
968
|
return true;
|
|
637
969
|
} catch (err) {
|
|
638
970
|
console.warn("Failed to wake instance:", err);
|
|
@@ -643,57 +975,13 @@ var PicoBaseClient = class {
|
|
|
643
975
|
* Get the current auth token (if signed in), or empty string.
|
|
644
976
|
*/
|
|
645
977
|
get token() {
|
|
646
|
-
return this.
|
|
978
|
+
return this._authStore.token;
|
|
647
979
|
}
|
|
648
980
|
/**
|
|
649
981
|
* Check if a user is currently authenticated.
|
|
650
982
|
*/
|
|
651
983
|
get isAuthenticated() {
|
|
652
|
-
return this.
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Monkey-patch pb.send to retry on 503 (cold start).
|
|
656
|
-
*
|
|
657
|
-
* When an instance is stopped or starting, the proxy returns 503.
|
|
658
|
-
* The SDK automatically retries with exponential backoff so the developer
|
|
659
|
-
* doesn't have to handle cold-start logic.
|
|
660
|
-
*/
|
|
661
|
-
_wrapSendWithRetry() {
|
|
662
|
-
const originalSend = this.pb.send.bind(this.pb);
|
|
663
|
-
const maxRetries = this.options.maxColdStartRetries;
|
|
664
|
-
this.pb.send = async (path, options) => {
|
|
665
|
-
let lastError;
|
|
666
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
667
|
-
try {
|
|
668
|
-
return await originalSend(path, options);
|
|
669
|
-
} catch (err) {
|
|
670
|
-
lastError = err;
|
|
671
|
-
const status = err?.status;
|
|
672
|
-
if (status === 503 && attempt < maxRetries) {
|
|
673
|
-
const delay = Math.pow(2, attempt + 1) * 1e3;
|
|
674
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (status === 401) {
|
|
678
|
-
const data = err?.data;
|
|
679
|
-
if (data?.code === "INVALID_API_KEY") {
|
|
680
|
-
throw new AuthorizationError();
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
if (status === 404) {
|
|
684
|
-
const msg = err?.message ?? "";
|
|
685
|
-
if (msg.toLowerCase().includes("missing collection") || msg.toLowerCase().includes("not found collection")) {
|
|
686
|
-
const match = msg.match(/["']([^"']+)["']/);
|
|
687
|
-
throw new CollectionNotFoundError(match?.[1] ?? "unknown");
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
throw err;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
throw new InstanceUnavailableError(
|
|
694
|
-
`Instance unavailable after ${maxRetries} retries. Original error: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
695
|
-
);
|
|
696
|
-
};
|
|
984
|
+
return this._authStore.isValid;
|
|
697
985
|
}
|
|
698
986
|
};
|
|
699
987
|
function createClient(urlOrOptions, apiKeyOrUndefined, options) {
|