@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.js CHANGED
@@ -1,136 +1,464 @@
1
1
  'use strict';
2
2
 
3
- var PocketBase = require('pocketbase');
4
-
5
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
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
- var PocketBase__default = /*#__PURE__*/_interopDefault(PocketBase);
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/client.ts
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(pb) {
324
+ constructor(http, authStore) {
14
325
  this.listeners = /* @__PURE__ */ new Set();
15
- this._collection = "users";
16
- this.pb = pb;
17
- this.pb.authStore.onChange((token) => {
18
- const record = this.pb.authStore.record ?? null;
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 'users'. Use this if you have a custom auth collection.
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.pb.collection(this._collection).create({
37
- email,
38
- password,
39
- passwordConfirm: passwordConfirm ?? password,
40
- ...rest
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
- const authResult = await this.pb.collection(this._collection).authWithPassword(email, password);
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.pb.collection(this._collection).authWithPassword(options.email, options.password);
53
- return {
54
- token: result.token,
55
- record: result.record
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.pb.collection(this._collection).authWithOAuth2({
66
- provider: options.provider,
67
- scopes: options.scopes,
68
- createData: options.createData,
69
- urlCallback: options.urlCallback
70
- });
71
- return {
72
- token: result.token,
73
- record: result.record
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.pb.collection(this._collection).authRefresh();
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.pb.collection(this._collection).requestPasswordReset(email);
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.pb.collection(this._collection).confirmPasswordReset(token, password, passwordConfirm ?? password);
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.pb.collection(this._collection).requestVerification(email);
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.pb.collection(this._collection).confirmVerification(token);
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.pb.authStore.clear();
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.pb.authStore.record;
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.pb.authStore.token;
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.pb.authStore.isValid;
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(pb, name) {
169
- this.pb = pb;
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
- return this.pb.collection(this.name).getList(page, perPage, options);
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
- return this.pb.collection(this.name).getFullList(options);
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
- return this.pb.collection(this.name).getOne(id, options);
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
- return this.pb.collection(this.name).getFirstListItem(filter, options);
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
- return this.pb.collection(this.name).create(data, options);
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
- return this.pb.collection(this.name).update(id, data, options);
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
- return this.pb.collection(this.name).delete(id);
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
- const topic = "*";
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
- await this.pb.collection(this.name).subscribe(id, callback);
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(pb) {
268
- this.pb = pb;
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
- await this.pb.collection(collection).subscribe("*", callback);
279
- return () => this.pb.collection(collection).unsubscribe("*");
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
- await this.pb.collection(collection).subscribe(recordId, callback);
291
- return () => this.pb.collection(collection).unsubscribe(recordId);
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
- await this.pb.collection(collection).unsubscribe();
686
+ this.subs.delete(collection);
687
+ this._pruneIfEmpty();
298
688
  }
299
689
  /**
300
- * Unsubscribe from ALL realtime events. The SSE connection will be
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
- await this.pb.realtime.unsubscribe();
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(pb) {
311
- this.pb = pb;
787
+ constructor(baseUrl, http) {
788
+ this.baseUrl = baseUrl;
789
+ this.http = http;
312
790
  }
313
791
  /**
314
- * Get the public URL for a file attached to a record.
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 queryParams = {};
322
- if (options.thumb) {
323
- queryParams["thumb"] = options.thumb;
324
- }
325
- if (options.token) {
326
- queryParams["token"] = options.token;
327
- }
328
- if (options.download) {
329
- queryParams["download"] = "1";
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
- return this.pb.files.getToken();
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(pb) {
346
- this.pb = pb;
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.pb.send("/api/collections", {
353
- method: "GET"
354
- });
355
- return result.items || [];
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.pb.send(`/api/collections/${idOrName}`, {
362
- method: "GET"
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.pb.send("/api/collections", {
370
- method: "POST",
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.pb.send(`/api/collections/${idOrName}`, {
379
- method: "PATCH",
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.pb.send(`/api/collections/${idOrName}`, {
389
- method: "DELETE"
390
- });
872
+ await this.http.send(
873
+ `/api/db/collections/${idOrName}`,
874
+ { method: "DELETE" }
875
+ );
391
876
  return true;
392
- } catch (e) {
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
- this.apiKey = apiKey;
548
- this.options = { ...DEFAULT_OPTIONS, ...options };
549
- const baseUrl = url.replace(/\/+$/, "");
550
- this.pb = new PocketBase__default.default(baseUrl);
551
- this.pb.autoCancellation(false);
552
- if (this.options.lang) {
553
- this.pb.lang = this.options.lang;
554
- }
555
- this.pb.beforeSend = (url2, reqInit) => {
556
- const headers = reqInit.headers ?? {};
557
- headers["X-PicoBase-Key"] = this.apiKey;
558
- reqInit.headers = headers;
559
- return { url: url2, options: reqInit };
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.pb, name);
931
+ return new PicoBaseCollection(this._http, name, this.realtime);
577
932
  }
578
933
  /**
579
- * Call a server-side function (PocketBase custom API endpoint).
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.pb.send(path, options ?? {});
937
+ return this._http.send(path, options);
584
938
  }
585
939
  /**
586
- * Call a remote procedure (RPC) - a convenience method for calling custom API endpoints.
940
+ * Call a remote procedure (RPC) convenience wrapper for custom API endpoints.
587
941
  *
588
- * Maps Supabase-style RPC calls to PicoBase custom endpoints:
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?.data;
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.pb.authStore.token;
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.pb.authStore.isValid;
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) {