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