@picobase_app/client 0.1.0 → 0.1.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/README.md CHANGED
@@ -145,6 +145,39 @@ const unsub = await pb.realtime.subscribe('posts', (event) => {
145
145
  await pb.realtime.disconnectAll()
146
146
  ```
147
147
 
148
+ ## RPC (Remote Procedure Calls)
149
+
150
+ Call custom server-side functions using the `.rpc()` method. This is especially useful for Supabase migrations.
151
+
152
+ ```typescript
153
+ // Simple RPC call
154
+ const result = await pb.rpc('calculate_cart_total', {
155
+ cart_id: '123'
156
+ })
157
+
158
+ // Complex RPC with typed response
159
+ interface DashboardStats {
160
+ posts: number
161
+ comments: number
162
+ followers: number
163
+ }
164
+
165
+ const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
166
+ user_id: currentUser.id
167
+ })
168
+ // stats.posts, stats.comments, stats.followers are typed!
169
+
170
+ // Common patterns
171
+ await pb.rpc('increment_views', { post_id: '123' })
172
+ const results = await pb.rpc('search_products', {
173
+ query: 'laptop',
174
+ min_price: 500,
175
+ category: 'electronics'
176
+ })
177
+ ```
178
+
179
+ RPC calls are mapped to custom PocketBase endpoints at `/api/rpc/{functionName}`. You'll need to implement these routes in your PocketBase instance. See the [PocketBase routing docs](https://pocketbase.io/docs/js-routing/) for details.
180
+
148
181
  ## File Storage
149
182
 
150
183
  PocketBase stores files as fields on records. Use the storage module to get URLs.
@@ -197,20 +230,54 @@ const pb = createClient('https://myapp.picobase.com', 'pbk_...', {
197
230
 
198
231
  ### Error handling
199
232
 
233
+ Every SDK error includes a `code` and `fix` property with actionable suggestions:
234
+
200
235
  ```typescript
201
- import { PicoBaseError, InstanceUnavailableError, AuthorizationError } from '@picobase_app/client'
236
+ import {
237
+ PicoBaseError,
238
+ AuthorizationError,
239
+ InstanceUnavailableError,
240
+ CollectionNotFoundError,
241
+ RecordNotFoundError,
242
+ ConfigurationError,
243
+ RpcError,
244
+ } from '@picobase_app/client'
202
245
 
203
246
  try {
204
247
  await pb.collection('posts').getList()
205
248
  } catch (err) {
206
- if (err instanceof AuthorizationError) {
207
- // Invalid API key
208
- } else if (err instanceof InstanceUnavailableError) {
209
- // Instance not available after retries
249
+ if (err instanceof PicoBaseError) {
250
+ console.log(err.message) // "Collection 'posts' not found."
251
+ console.log(err.code) // "COLLECTION_NOT_FOUND"
252
+ console.log(err.fix) // "Make sure the collection 'posts' exists..."
210
253
  }
211
254
  }
212
255
  ```
213
256
 
257
+ **Error types:**
258
+
259
+ | Error | Code | When |
260
+ |---|---|---|
261
+ | `ConfigurationError` | `CONFIGURATION_ERROR` | Missing URL, API key, or bad config |
262
+ | `AuthorizationError` | `UNAUTHORIZED` | Invalid or missing API key |
263
+ | `CollectionNotFoundError` | `COLLECTION_NOT_FOUND` | Collection doesn't exist |
264
+ | `RecordNotFoundError` | `RECORD_NOT_FOUND` | Record ID not found |
265
+ | `InstanceUnavailableError` | `INSTANCE_UNAVAILABLE` | Instance down after retries |
266
+ | `RpcError` | `RPC_ERROR` | RPC function call failed (includes endpoint-specific fix hints) |
267
+ | `RequestError` | `REQUEST_FAILED` | Generic HTTP error (includes status-specific fix hints) |
268
+
269
+ ### Typed collections with `picobase typegen`
270
+
271
+ Run `picobase typegen` to generate types from your schema. The generated file includes a typed client:
272
+
273
+ ```typescript
274
+ import { pb } from './src/types/picobase'
275
+
276
+ // Collection names autocomplete, record fields are typed
277
+ const result = await pb.collection('posts').getList(1, 20)
278
+ result.items[0].title // string — fully typed!
279
+ ```
280
+
214
281
  ## API Reference
215
282
 
216
283
  ### `createClient(url, apiKey, options?)`
package/dist/index.d.mts CHANGED
@@ -410,6 +410,33 @@ declare class PicoBaseClient {
410
410
  * Proxies to PocketBase's send() method.
411
411
  */
412
412
  send<T = unknown>(path: string, options?: SendOptions): Promise<T>;
413
+ /**
414
+ * Call a remote procedure (RPC) - a convenience method for calling custom API endpoints.
415
+ *
416
+ * Maps Supabase-style RPC calls to PicoBase custom endpoints:
417
+ * - `pb.rpc('my_function', params)` → `POST /api/rpc/my_function` with params as body
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * // Simple RPC call
422
+ * const result = await pb.rpc('calculate_total', { cart_id: '123' })
423
+ *
424
+ * // Complex RPC with typed response
425
+ * interface DashboardStats {
426
+ * posts: number
427
+ * comments: number
428
+ * followers: number
429
+ * }
430
+ * const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
431
+ * user_id: currentUser.id
432
+ * })
433
+ * ```
434
+ *
435
+ * @param functionName The name of the RPC function to call
436
+ * @param params Optional parameters to pass to the function
437
+ * @returns The function result
438
+ */
439
+ rpc<T = unknown>(functionName: string, params?: Record<string, unknown>): Promise<T>;
413
440
  /**
414
441
  * Get the current auth token (if signed in), or empty string.
415
442
  */
@@ -462,12 +489,21 @@ declare function createClient(url: string, apiKey: string, options?: PicoBaseCli
462
489
 
463
490
  /**
464
491
  * Base error class for all PicoBase SDK errors.
492
+ *
493
+ * Every error includes a `code` for programmatic handling and a `fix`
494
+ * suggestion so developers can resolve issues without digging through docs.
465
495
  */
466
496
  declare class PicoBaseError extends Error {
467
497
  readonly code: string;
468
498
  readonly status?: number | undefined;
469
499
  readonly details?: unknown | undefined;
470
- constructor(message: string, code: string, status?: number | undefined, details?: unknown | undefined);
500
+ /** Actionable suggestion for how to fix this error. */
501
+ readonly fix?: string | undefined;
502
+ constructor(message: string, code: string, status?: number | undefined, details?: unknown | undefined,
503
+ /** Actionable suggestion for how to fix this error. */
504
+ fix?: string | undefined);
505
+ /** Formatted error string including fix suggestion. */
506
+ toString(): string;
471
507
  }
472
508
  /**
473
509
  * Thrown when the instance is not running and cold-start retries are exhausted.
@@ -481,11 +517,35 @@ declare class InstanceUnavailableError extends PicoBaseError {
481
517
  declare class AuthorizationError extends PicoBaseError {
482
518
  constructor(message?: string);
483
519
  }
520
+ /**
521
+ * Thrown when a collection is not found.
522
+ */
523
+ declare class CollectionNotFoundError extends PicoBaseError {
524
+ constructor(collectionName: string);
525
+ }
526
+ /**
527
+ * Thrown when a record is not found.
528
+ */
529
+ declare class RecordNotFoundError extends PicoBaseError {
530
+ constructor(collectionName: string, recordId: string);
531
+ }
484
532
  /**
485
533
  * Thrown when a PocketBase API request fails.
486
534
  */
487
535
  declare class RequestError extends PicoBaseError {
488
536
  constructor(message: string, status: number, details?: unknown);
489
537
  }
538
+ /**
539
+ * Thrown when the SDK is misconfigured (bad URL, missing params, etc.).
540
+ */
541
+ declare class ConfigurationError extends PicoBaseError {
542
+ constructor(message: string, fix: string);
543
+ }
544
+ /**
545
+ * Thrown when an RPC (remote procedure call) fails.
546
+ */
547
+ declare class RpcError extends PicoBaseError {
548
+ constructor(functionName: string, status: number, details?: unknown);
549
+ }
490
550
 
491
- export { type AuthEvent, type AuthResponse, type AuthStateChange, type AuthStateChangeCallback, AuthorizationError, type FileOptions, InstanceUnavailableError, type ListOptions, type OAuthSignInOptions, PicoBaseAuth, PicoBaseClient, type PicoBaseClientOptions, PicoBaseCollection, PicoBaseError, PicoBaseRealtime, PicoBaseStorage, type RealtimeAction, type RealtimeCallback, type RecordQueryOptions, RequestError, type SignInOptions, type SignUpOptions, type UnsubscribeFunc, createClient };
551
+ export { type AuthEvent, type AuthResponse, type AuthStateChange, type AuthStateChangeCallback, AuthorizationError, CollectionNotFoundError, ConfigurationError, type FileOptions, InstanceUnavailableError, type ListOptions, type OAuthSignInOptions, PicoBaseAuth, PicoBaseClient, type PicoBaseClientOptions, PicoBaseCollection, PicoBaseError, PicoBaseRealtime, PicoBaseStorage, type RealtimeAction, type RealtimeCallback, RecordNotFoundError, type RecordQueryOptions, RequestError, RpcError, type SignInOptions, type SignUpOptions, type UnsubscribeFunc, createClient };
package/dist/index.d.ts CHANGED
@@ -410,6 +410,33 @@ declare class PicoBaseClient {
410
410
  * Proxies to PocketBase's send() method.
411
411
  */
412
412
  send<T = unknown>(path: string, options?: SendOptions): Promise<T>;
413
+ /**
414
+ * Call a remote procedure (RPC) - a convenience method for calling custom API endpoints.
415
+ *
416
+ * Maps Supabase-style RPC calls to PicoBase custom endpoints:
417
+ * - `pb.rpc('my_function', params)` → `POST /api/rpc/my_function` with params as body
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * // Simple RPC call
422
+ * const result = await pb.rpc('calculate_total', { cart_id: '123' })
423
+ *
424
+ * // Complex RPC with typed response
425
+ * interface DashboardStats {
426
+ * posts: number
427
+ * comments: number
428
+ * followers: number
429
+ * }
430
+ * const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
431
+ * user_id: currentUser.id
432
+ * })
433
+ * ```
434
+ *
435
+ * @param functionName The name of the RPC function to call
436
+ * @param params Optional parameters to pass to the function
437
+ * @returns The function result
438
+ */
439
+ rpc<T = unknown>(functionName: string, params?: Record<string, unknown>): Promise<T>;
413
440
  /**
414
441
  * Get the current auth token (if signed in), or empty string.
415
442
  */
@@ -462,12 +489,21 @@ declare function createClient(url: string, apiKey: string, options?: PicoBaseCli
462
489
 
463
490
  /**
464
491
  * Base error class for all PicoBase SDK errors.
492
+ *
493
+ * Every error includes a `code` for programmatic handling and a `fix`
494
+ * suggestion so developers can resolve issues without digging through docs.
465
495
  */
466
496
  declare class PicoBaseError extends Error {
467
497
  readonly code: string;
468
498
  readonly status?: number | undefined;
469
499
  readonly details?: unknown | undefined;
470
- constructor(message: string, code: string, status?: number | undefined, details?: unknown | undefined);
500
+ /** Actionable suggestion for how to fix this error. */
501
+ readonly fix?: string | undefined;
502
+ constructor(message: string, code: string, status?: number | undefined, details?: unknown | undefined,
503
+ /** Actionable suggestion for how to fix this error. */
504
+ fix?: string | undefined);
505
+ /** Formatted error string including fix suggestion. */
506
+ toString(): string;
471
507
  }
472
508
  /**
473
509
  * Thrown when the instance is not running and cold-start retries are exhausted.
@@ -481,11 +517,35 @@ declare class InstanceUnavailableError extends PicoBaseError {
481
517
  declare class AuthorizationError extends PicoBaseError {
482
518
  constructor(message?: string);
483
519
  }
520
+ /**
521
+ * Thrown when a collection is not found.
522
+ */
523
+ declare class CollectionNotFoundError extends PicoBaseError {
524
+ constructor(collectionName: string);
525
+ }
526
+ /**
527
+ * Thrown when a record is not found.
528
+ */
529
+ declare class RecordNotFoundError extends PicoBaseError {
530
+ constructor(collectionName: string, recordId: string);
531
+ }
484
532
  /**
485
533
  * Thrown when a PocketBase API request fails.
486
534
  */
487
535
  declare class RequestError extends PicoBaseError {
488
536
  constructor(message: string, status: number, details?: unknown);
489
537
  }
538
+ /**
539
+ * Thrown when the SDK is misconfigured (bad URL, missing params, etc.).
540
+ */
541
+ declare class ConfigurationError extends PicoBaseError {
542
+ constructor(message: string, fix: string);
543
+ }
544
+ /**
545
+ * Thrown when an RPC (remote procedure call) fails.
546
+ */
547
+ declare class RpcError extends PicoBaseError {
548
+ constructor(functionName: string, status: number, details?: unknown);
549
+ }
490
550
 
491
- export { type AuthEvent, type AuthResponse, type AuthStateChange, type AuthStateChangeCallback, AuthorizationError, type FileOptions, InstanceUnavailableError, type ListOptions, type OAuthSignInOptions, PicoBaseAuth, PicoBaseClient, type PicoBaseClientOptions, PicoBaseCollection, PicoBaseError, PicoBaseRealtime, PicoBaseStorage, type RealtimeAction, type RealtimeCallback, type RecordQueryOptions, RequestError, type SignInOptions, type SignUpOptions, type UnsubscribeFunc, createClient };
551
+ export { type AuthEvent, type AuthResponse, type AuthStateChange, type AuthStateChangeCallback, AuthorizationError, CollectionNotFoundError, ConfigurationError, type FileOptions, InstanceUnavailableError, type ListOptions, type OAuthSignInOptions, PicoBaseAuth, PicoBaseClient, type PicoBaseClientOptions, PicoBaseCollection, PicoBaseError, PicoBaseRealtime, PicoBaseStorage, type RealtimeAction, type RealtimeCallback, RecordNotFoundError, type RecordQueryOptions, RequestError, RpcError, type SignInOptions, type SignUpOptions, type UnsubscribeFunc, createClient };
package/dist/index.js CHANGED
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AuthorizationError: () => AuthorizationError,
34
+ CollectionNotFoundError: () => CollectionNotFoundError,
35
+ ConfigurationError: () => ConfigurationError,
34
36
  InstanceUnavailableError: () => InstanceUnavailableError,
35
37
  PicoBaseAuth: () => PicoBaseAuth,
36
38
  PicoBaseClient: () => PicoBaseClient,
@@ -38,7 +40,9 @@ __export(index_exports, {
38
40
  PicoBaseError: () => PicoBaseError,
39
41
  PicoBaseRealtime: () => PicoBaseRealtime,
40
42
  PicoBaseStorage: () => PicoBaseStorage,
43
+ RecordNotFoundError: () => RecordNotFoundError,
41
44
  RequestError: () => RequestError,
45
+ RpcError: () => RpcError,
42
46
  createClient: () => createClient
43
47
  });
44
48
  module.exports = __toCommonJS(index_exports);
@@ -380,32 +384,126 @@ var PicoBaseStorage = class {
380
384
 
381
385
  // src/errors.ts
382
386
  var PicoBaseError = class extends Error {
383
- constructor(message, code, status, details) {
387
+ constructor(message, code, status, details, fix) {
384
388
  super(message);
385
389
  this.code = code;
386
390
  this.status = status;
387
391
  this.details = details;
392
+ this.fix = fix;
388
393
  this.name = "PicoBaseError";
389
394
  }
395
+ /** Formatted error string including fix suggestion. */
396
+ toString() {
397
+ let s = `${this.name} [${this.code}]: ${this.message}`;
398
+ if (this.fix) s += `
399
+ Fix: ${this.fix}`;
400
+ return s;
401
+ }
390
402
  };
391
403
  var InstanceUnavailableError = class extends PicoBaseError {
392
404
  constructor(message = "Instance is not available. It may be stopped or starting up.") {
393
- super(message, "INSTANCE_UNAVAILABLE", 503);
405
+ super(
406
+ message,
407
+ "INSTANCE_UNAVAILABLE",
408
+ 503,
409
+ void 0,
410
+ "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`."
411
+ );
394
412
  this.name = "InstanceUnavailableError";
395
413
  }
396
414
  };
397
415
  var AuthorizationError = class extends PicoBaseError {
398
416
  constructor(message = "Invalid or missing API key.") {
399
- super(message, "UNAUTHORIZED", 401);
417
+ super(
418
+ message,
419
+ "UNAUTHORIZED",
420
+ 401,
421
+ void 0,
422
+ '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.'
423
+ );
400
424
  this.name = "AuthorizationError";
401
425
  }
402
426
  };
427
+ var CollectionNotFoundError = class extends PicoBaseError {
428
+ constructor(collectionName) {
429
+ super(
430
+ `Collection "${collectionName}" not found.`,
431
+ "COLLECTION_NOT_FOUND",
432
+ 404,
433
+ { collection: collectionName },
434
+ `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.`
435
+ );
436
+ this.name = "CollectionNotFoundError";
437
+ }
438
+ };
439
+ var RecordNotFoundError = class extends PicoBaseError {
440
+ constructor(collectionName, recordId) {
441
+ super(
442
+ `Record "${recordId}" not found in collection "${collectionName}".`,
443
+ "RECORD_NOT_FOUND",
444
+ 404,
445
+ { collection: collectionName, recordId },
446
+ 'Check that the record ID is correct. IDs are 15-character alphanumeric strings (e.g., "abc123def456789").'
447
+ );
448
+ this.name = "RecordNotFoundError";
449
+ }
450
+ };
403
451
  var RequestError = class extends PicoBaseError {
404
452
  constructor(message, status, details) {
405
- super(message, "REQUEST_FAILED", status, details);
453
+ const fix = requestErrorFix(status, message);
454
+ super(message, "REQUEST_FAILED", status, details, fix);
406
455
  this.name = "RequestError";
407
456
  }
408
457
  };
458
+ var ConfigurationError = class extends PicoBaseError {
459
+ constructor(message, fix) {
460
+ super(message, "CONFIGURATION_ERROR", void 0, void 0, fix);
461
+ this.name = "ConfigurationError";
462
+ }
463
+ };
464
+ var RpcError = class extends PicoBaseError {
465
+ constructor(functionName, status, details) {
466
+ const fix = rpcErrorFix(functionName, status);
467
+ super(
468
+ `RPC function "${functionName}" failed.`,
469
+ "RPC_ERROR",
470
+ status,
471
+ details,
472
+ fix
473
+ );
474
+ this.name = "RpcError";
475
+ }
476
+ };
477
+ function rpcErrorFix(functionName, status) {
478
+ if (status === 404) {
479
+ 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/`;
480
+ }
481
+ if (status === 400) {
482
+ return "Check the parameters you are passing to this RPC function. The function may be expecting different parameters or types.";
483
+ }
484
+ if (status === 403) {
485
+ return "You don't have permission to call this RPC function. Check the authentication requirements for this endpoint in your PocketBase routes.";
486
+ }
487
+ return "Check your PicoBase instance logs for details about this RPC error. Ensure the custom route is correctly implemented in your PocketBase setup.";
488
+ }
489
+ function requestErrorFix(status, message) {
490
+ switch (status) {
491
+ case 400:
492
+ 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.";
493
+ case 403:
494
+ 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.";
495
+ case 404:
496
+ if (message.toLowerCase().includes("collection"))
497
+ return "This collection does not exist yet. Write a record to auto-create it, or create it in the dashboard.";
498
+ return "The requested resource was not found. Double-check IDs and collection names.";
499
+ case 413:
500
+ return "The request payload is too large. Check file upload size limits in your instance settings.";
501
+ case 429:
502
+ return "Too many requests. Add a short delay between requests or implement client-side caching.";
503
+ default:
504
+ return "If this error persists, check your PicoBase dashboard for instance health and logs.";
505
+ }
506
+ }
409
507
 
410
508
  // src/client.ts
411
509
  var DEFAULT_OPTIONS = {
@@ -415,6 +513,24 @@ var DEFAULT_OPTIONS = {
415
513
  };
416
514
  var PicoBaseClient = class {
417
515
  constructor(url, apiKey, options = {}) {
516
+ if (!url) {
517
+ throw new ConfigurationError(
518
+ "PicoBase URL is required.",
519
+ 'Pass the URL as the first argument: createClient("https://myapp.picobase.com", "pbk_...") or set PICOBASE_URL in your .env file.'
520
+ );
521
+ }
522
+ if (!apiKey) {
523
+ throw new ConfigurationError(
524
+ "PicoBase API key is required.",
525
+ 'Pass the API key as the second argument: createClient("https://...", "pbk_your_key") or set PICOBASE_API_KEY in your .env file. Get a key from your dashboard.'
526
+ );
527
+ }
528
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
529
+ throw new ConfigurationError(
530
+ `Invalid URL: "${url}". Must start with http:// or https://.`,
531
+ `Use the full URL: createClient("https://${url}", "...")`
532
+ );
533
+ }
418
534
  this.apiKey = apiKey;
419
535
  this.options = { ...DEFAULT_OPTIONS, ...options };
420
536
  const baseUrl = url.replace(/\/+$/, "");
@@ -452,6 +568,44 @@ var PicoBaseClient = class {
452
568
  async send(path, options) {
453
569
  return this.pb.send(path, options ?? {});
454
570
  }
571
+ /**
572
+ * Call a remote procedure (RPC) - a convenience method for calling custom API endpoints.
573
+ *
574
+ * Maps Supabase-style RPC calls to PicoBase custom endpoints:
575
+ * - `pb.rpc('my_function', params)` → `POST /api/rpc/my_function` with params as body
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * // Simple RPC call
580
+ * const result = await pb.rpc('calculate_total', { cart_id: '123' })
581
+ *
582
+ * // Complex RPC with typed response
583
+ * interface DashboardStats {
584
+ * posts: number
585
+ * comments: number
586
+ * followers: number
587
+ * }
588
+ * const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
589
+ * user_id: currentUser.id
590
+ * })
591
+ * ```
592
+ *
593
+ * @param functionName The name of the RPC function to call
594
+ * @param params Optional parameters to pass to the function
595
+ * @returns The function result
596
+ */
597
+ async rpc(functionName, params) {
598
+ try {
599
+ return await this.send(`/api/rpc/${functionName}`, {
600
+ method: "POST",
601
+ body: params ?? {}
602
+ });
603
+ } catch (err) {
604
+ const status = err?.status ?? 500;
605
+ const details = err?.data;
606
+ throw new RpcError(functionName, status, details);
607
+ }
608
+ }
455
609
  /**
456
610
  * Get the current auth token (if signed in), or empty string.
457
611
  */
@@ -493,6 +647,13 @@ var PicoBaseClient = class {
493
647
  throw new AuthorizationError();
494
648
  }
495
649
  }
650
+ if (status === 404) {
651
+ const msg = err?.message ?? "";
652
+ if (msg.toLowerCase().includes("missing collection") || msg.toLowerCase().includes("not found collection")) {
653
+ const match = msg.match(/["']([^"']+)["']/);
654
+ throw new CollectionNotFoundError(match?.[1] ?? "unknown");
655
+ }
656
+ }
496
657
  throw err;
497
658
  }
498
659
  }
@@ -508,8 +669,13 @@ function createClient(urlOrOptions, apiKeyOrUndefined, options) {
508
669
  const url = env.PICOBASE_URL || env.NEXT_PUBLIC_PICOBASE_URL;
509
670
  const apiKey = env.PICOBASE_API_KEY || env.NEXT_PUBLIC_PICOBASE_API_KEY;
510
671
  if (!url || !apiKey) {
511
- throw new Error(
512
- "createClient() called without arguments, but PICOBASE_URL and PICOBASE_API_KEY environment variables are not set. Either pass them explicitly or add them to your .env file."
672
+ const missing = [
673
+ !url && "PICOBASE_URL (or NEXT_PUBLIC_PICOBASE_URL)",
674
+ !apiKey && "PICOBASE_API_KEY (or NEXT_PUBLIC_PICOBASE_API_KEY)"
675
+ ].filter(Boolean).join(" and ");
676
+ throw new ConfigurationError(
677
+ `Missing environment variable${!url && !apiKey ? "s" : ""}: ${missing}`,
678
+ "Add them to your .env.local file:\n\n PICOBASE_URL=https://your-app.picobase.com\n PICOBASE_API_KEY=pbk_your_key_here\n\nOr for Next.js (client-side access), prefix with NEXT_PUBLIC_:\n\n NEXT_PUBLIC_PICOBASE_URL=https://your-app.picobase.com\n NEXT_PUBLIC_PICOBASE_API_KEY=pbk_your_key_here\n\nGet your URL and API key from: https://picobase.com/dashboard\nOr run: picobase init"
513
679
  );
514
680
  }
515
681
  return new PicoBaseClient(url, apiKey, urlOrOptions);
@@ -519,6 +685,8 @@ function createClient(urlOrOptions, apiKeyOrUndefined, options) {
519
685
  // Annotate the CommonJS export names for ESM import in node:
520
686
  0 && (module.exports = {
521
687
  AuthorizationError,
688
+ CollectionNotFoundError,
689
+ ConfigurationError,
522
690
  InstanceUnavailableError,
523
691
  PicoBaseAuth,
524
692
  PicoBaseClient,
@@ -526,6 +694,8 @@ function createClient(urlOrOptions, apiKeyOrUndefined, options) {
526
694
  PicoBaseError,
527
695
  PicoBaseRealtime,
528
696
  PicoBaseStorage,
697
+ RecordNotFoundError,
529
698
  RequestError,
699
+ RpcError,
530
700
  createClient
531
701
  });
package/dist/index.mjs CHANGED
@@ -335,32 +335,126 @@ var PicoBaseStorage = class {
335
335
 
336
336
  // src/errors.ts
337
337
  var PicoBaseError = class extends Error {
338
- constructor(message, code, status, details) {
338
+ constructor(message, code, status, details, fix) {
339
339
  super(message);
340
340
  this.code = code;
341
341
  this.status = status;
342
342
  this.details = details;
343
+ this.fix = fix;
343
344
  this.name = "PicoBaseError";
344
345
  }
346
+ /** Formatted error string including fix suggestion. */
347
+ toString() {
348
+ let s = `${this.name} [${this.code}]: ${this.message}`;
349
+ if (this.fix) s += `
350
+ Fix: ${this.fix}`;
351
+ return s;
352
+ }
345
353
  };
346
354
  var InstanceUnavailableError = class extends PicoBaseError {
347
355
  constructor(message = "Instance is not available. It may be stopped or starting up.") {
348
- super(message, "INSTANCE_UNAVAILABLE", 503);
356
+ super(
357
+ message,
358
+ "INSTANCE_UNAVAILABLE",
359
+ 503,
360
+ void 0,
361
+ "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`."
362
+ );
349
363
  this.name = "InstanceUnavailableError";
350
364
  }
351
365
  };
352
366
  var AuthorizationError = class extends PicoBaseError {
353
367
  constructor(message = "Invalid or missing API key.") {
354
- super(message, "UNAUTHORIZED", 401);
368
+ super(
369
+ message,
370
+ "UNAUTHORIZED",
371
+ 401,
372
+ void 0,
373
+ '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.'
374
+ );
355
375
  this.name = "AuthorizationError";
356
376
  }
357
377
  };
378
+ var CollectionNotFoundError = class extends PicoBaseError {
379
+ constructor(collectionName) {
380
+ super(
381
+ `Collection "${collectionName}" not found.`,
382
+ "COLLECTION_NOT_FOUND",
383
+ 404,
384
+ { collection: collectionName },
385
+ `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.`
386
+ );
387
+ this.name = "CollectionNotFoundError";
388
+ }
389
+ };
390
+ var RecordNotFoundError = class extends PicoBaseError {
391
+ constructor(collectionName, recordId) {
392
+ super(
393
+ `Record "${recordId}" not found in collection "${collectionName}".`,
394
+ "RECORD_NOT_FOUND",
395
+ 404,
396
+ { collection: collectionName, recordId },
397
+ 'Check that the record ID is correct. IDs are 15-character alphanumeric strings (e.g., "abc123def456789").'
398
+ );
399
+ this.name = "RecordNotFoundError";
400
+ }
401
+ };
358
402
  var RequestError = class extends PicoBaseError {
359
403
  constructor(message, status, details) {
360
- super(message, "REQUEST_FAILED", status, details);
404
+ const fix = requestErrorFix(status, message);
405
+ super(message, "REQUEST_FAILED", status, details, fix);
361
406
  this.name = "RequestError";
362
407
  }
363
408
  };
409
+ var ConfigurationError = class extends PicoBaseError {
410
+ constructor(message, fix) {
411
+ super(message, "CONFIGURATION_ERROR", void 0, void 0, fix);
412
+ this.name = "ConfigurationError";
413
+ }
414
+ };
415
+ var RpcError = class extends PicoBaseError {
416
+ constructor(functionName, status, details) {
417
+ const fix = rpcErrorFix(functionName, status);
418
+ super(
419
+ `RPC function "${functionName}" failed.`,
420
+ "RPC_ERROR",
421
+ status,
422
+ details,
423
+ fix
424
+ );
425
+ this.name = "RpcError";
426
+ }
427
+ };
428
+ function rpcErrorFix(functionName, status) {
429
+ if (status === 404) {
430
+ 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/`;
431
+ }
432
+ if (status === 400) {
433
+ return "Check the parameters you are passing to this RPC function. The function may be expecting different parameters or types.";
434
+ }
435
+ if (status === 403) {
436
+ return "You don't have permission to call this RPC function. Check the authentication requirements for this endpoint in your PocketBase routes.";
437
+ }
438
+ return "Check your PicoBase instance logs for details about this RPC error. Ensure the custom route is correctly implemented in your PocketBase setup.";
439
+ }
440
+ function requestErrorFix(status, message) {
441
+ switch (status) {
442
+ case 400:
443
+ 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.";
444
+ case 403:
445
+ 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.";
446
+ case 404:
447
+ if (message.toLowerCase().includes("collection"))
448
+ return "This collection does not exist yet. Write a record to auto-create it, or create it in the dashboard.";
449
+ return "The requested resource was not found. Double-check IDs and collection names.";
450
+ case 413:
451
+ return "The request payload is too large. Check file upload size limits in your instance settings.";
452
+ case 429:
453
+ return "Too many requests. Add a short delay between requests or implement client-side caching.";
454
+ default:
455
+ return "If this error persists, check your PicoBase dashboard for instance health and logs.";
456
+ }
457
+ }
364
458
 
365
459
  // src/client.ts
366
460
  var DEFAULT_OPTIONS = {
@@ -370,6 +464,24 @@ var DEFAULT_OPTIONS = {
370
464
  };
371
465
  var PicoBaseClient = class {
372
466
  constructor(url, apiKey, options = {}) {
467
+ if (!url) {
468
+ throw new ConfigurationError(
469
+ "PicoBase URL is required.",
470
+ 'Pass the URL as the first argument: createClient("https://myapp.picobase.com", "pbk_...") or set PICOBASE_URL in your .env file.'
471
+ );
472
+ }
473
+ if (!apiKey) {
474
+ throw new ConfigurationError(
475
+ "PicoBase API key is required.",
476
+ 'Pass the API key as the second argument: createClient("https://...", "pbk_your_key") or set PICOBASE_API_KEY in your .env file. Get a key from your dashboard.'
477
+ );
478
+ }
479
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
480
+ throw new ConfigurationError(
481
+ `Invalid URL: "${url}". Must start with http:// or https://.`,
482
+ `Use the full URL: createClient("https://${url}", "...")`
483
+ );
484
+ }
373
485
  this.apiKey = apiKey;
374
486
  this.options = { ...DEFAULT_OPTIONS, ...options };
375
487
  const baseUrl = url.replace(/\/+$/, "");
@@ -407,6 +519,44 @@ var PicoBaseClient = class {
407
519
  async send(path, options) {
408
520
  return this.pb.send(path, options ?? {});
409
521
  }
522
+ /**
523
+ * Call a remote procedure (RPC) - a convenience method for calling custom API endpoints.
524
+ *
525
+ * Maps Supabase-style RPC calls to PicoBase custom endpoints:
526
+ * - `pb.rpc('my_function', params)` → `POST /api/rpc/my_function` with params as body
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * // Simple RPC call
531
+ * const result = await pb.rpc('calculate_total', { cart_id: '123' })
532
+ *
533
+ * // Complex RPC with typed response
534
+ * interface DashboardStats {
535
+ * posts: number
536
+ * comments: number
537
+ * followers: number
538
+ * }
539
+ * const stats = await pb.rpc<DashboardStats>('get_dashboard_stats', {
540
+ * user_id: currentUser.id
541
+ * })
542
+ * ```
543
+ *
544
+ * @param functionName The name of the RPC function to call
545
+ * @param params Optional parameters to pass to the function
546
+ * @returns The function result
547
+ */
548
+ async rpc(functionName, params) {
549
+ try {
550
+ return await this.send(`/api/rpc/${functionName}`, {
551
+ method: "POST",
552
+ body: params ?? {}
553
+ });
554
+ } catch (err) {
555
+ const status = err?.status ?? 500;
556
+ const details = err?.data;
557
+ throw new RpcError(functionName, status, details);
558
+ }
559
+ }
410
560
  /**
411
561
  * Get the current auth token (if signed in), or empty string.
412
562
  */
@@ -448,6 +598,13 @@ var PicoBaseClient = class {
448
598
  throw new AuthorizationError();
449
599
  }
450
600
  }
601
+ if (status === 404) {
602
+ const msg = err?.message ?? "";
603
+ if (msg.toLowerCase().includes("missing collection") || msg.toLowerCase().includes("not found collection")) {
604
+ const match = msg.match(/["']([^"']+)["']/);
605
+ throw new CollectionNotFoundError(match?.[1] ?? "unknown");
606
+ }
607
+ }
451
608
  throw err;
452
609
  }
453
610
  }
@@ -463,8 +620,13 @@ function createClient(urlOrOptions, apiKeyOrUndefined, options) {
463
620
  const url = env.PICOBASE_URL || env.NEXT_PUBLIC_PICOBASE_URL;
464
621
  const apiKey = env.PICOBASE_API_KEY || env.NEXT_PUBLIC_PICOBASE_API_KEY;
465
622
  if (!url || !apiKey) {
466
- throw new Error(
467
- "createClient() called without arguments, but PICOBASE_URL and PICOBASE_API_KEY environment variables are not set. Either pass them explicitly or add them to your .env file."
623
+ const missing = [
624
+ !url && "PICOBASE_URL (or NEXT_PUBLIC_PICOBASE_URL)",
625
+ !apiKey && "PICOBASE_API_KEY (or NEXT_PUBLIC_PICOBASE_API_KEY)"
626
+ ].filter(Boolean).join(" and ");
627
+ throw new ConfigurationError(
628
+ `Missing environment variable${!url && !apiKey ? "s" : ""}: ${missing}`,
629
+ "Add them to your .env.local file:\n\n PICOBASE_URL=https://your-app.picobase.com\n PICOBASE_API_KEY=pbk_your_key_here\n\nOr for Next.js (client-side access), prefix with NEXT_PUBLIC_:\n\n NEXT_PUBLIC_PICOBASE_URL=https://your-app.picobase.com\n NEXT_PUBLIC_PICOBASE_API_KEY=pbk_your_key_here\n\nGet your URL and API key from: https://picobase.com/dashboard\nOr run: picobase init"
468
630
  );
469
631
  }
470
632
  return new PicoBaseClient(url, apiKey, urlOrOptions);
@@ -473,6 +635,8 @@ function createClient(urlOrOptions, apiKeyOrUndefined, options) {
473
635
  }
474
636
  export {
475
637
  AuthorizationError,
638
+ CollectionNotFoundError,
639
+ ConfigurationError,
476
640
  InstanceUnavailableError,
477
641
  PicoBaseAuth,
478
642
  PicoBaseClient,
@@ -480,6 +644,8 @@ export {
480
644
  PicoBaseError,
481
645
  PicoBaseRealtime,
482
646
  PicoBaseStorage,
647
+ RecordNotFoundError,
483
648
  RequestError,
649
+ RpcError,
484
650
  createClient
485
651
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picobase_app/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "PicoBase client SDK — auth, database, storage, and realtime for your apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -18,15 +18,25 @@
18
18
  "scripts": {
19
19
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
20
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "test": "vitest",
22
+ "test:watch": "vitest --watch",
23
+ "test:ui": "vitest --ui",
21
24
  "typecheck": "tsc --noEmit",
22
- "prepublishOnly": "npm run build"
25
+ "prepublishOnly": "npm run build",
26
+ "publish:dry": "npm publish --dry-run",
27
+ "release:patch": "npm version patch && npm publish",
28
+ "release:minor": "npm version minor && npm publish",
29
+ "release:major": "npm version major && npm publish"
23
30
  },
24
31
  "dependencies": {
25
32
  "pocketbase": "^0.25.2"
26
33
  },
27
34
  "devDependencies": {
35
+ "@types/node": "^25.2.3",
36
+ "@vitest/ui": "^2.1.8",
28
37
  "tsup": "^8.4.0",
29
- "typescript": "^5.7.3"
38
+ "typescript": "^5.7.3",
39
+ "vitest": "^2.1.8"
30
40
  },
31
41
  "keywords": [
32
42
  "picobase",