@everystack/api 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/api",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "PostgREST-compatible HTTP handler + typed fetch client for Drizzle ORM",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
@@ -76,6 +76,8 @@ export interface QueryBuilder {
76
76
 
77
77
  export interface Client {
78
78
  from(table: string): QueryBuilder;
79
+ /** Call a named RPC on the handler: POST {baseUrl}/rpc/{fn}. */
80
+ rpc<T = unknown>(fn: string, body?: unknown): Promise<ClientResponse<T>>;
79
81
  }
80
82
 
81
83
  /**
@@ -89,6 +91,27 @@ export function createClient(options: ClientOptions): Client {
89
91
  from(table: string): QueryBuilder {
90
92
  return createQueryBuilder(options, table);
91
93
  },
94
+
95
+ async rpc<T = unknown>(fn: string, body?: unknown): Promise<ClientResponse<T>> {
96
+ const url = `${options.baseUrl}/rpc/${encodeURIComponent(fn)}`;
97
+ const headers = await buildHeaders(options);
98
+ headers['Content-Type'] = 'application/json';
99
+
100
+ const { response, retried } = await fetchWithRetry(
101
+ url,
102
+ { method: 'POST', headers, body: JSON.stringify(body ?? {}) },
103
+ options,
104
+ );
105
+
106
+ if (retried) return retried as ClientResponse<T>;
107
+
108
+ if (!response.ok) {
109
+ return { data: null, error: await parseError(response) };
110
+ }
111
+
112
+ const data = (await response.json()) as T;
113
+ return { data, error: null };
114
+ },
92
115
  };
93
116
  }
94
117
 
@@ -108,14 +108,25 @@ export interface CacheConfig {
108
108
  private?: string;
109
109
  }
110
110
 
111
- /** RPC handler — plain function (backward compatible) */
112
- export type RpcHandler = (body: unknown, user?: Record<string, unknown> | null, client?: Record<string, unknown> | null) => Promise<unknown>;
113
-
114
- /** RPC handler with per-function role requirement */
111
+ /** RPC handler — plain function (backward compatible).
112
+ * Plain handlers dispatch directly: no transaction, no pgSettings — they own
113
+ * their DB access through whatever they close over, same privilege model as
114
+ * SSR loaders. The optional 4th argument is only provided to handlers
115
+ * registered with `settings: true` (see RpcHandlerWithRole). */
116
+ export type RpcHandler = (body: unknown, user?: Record<string, unknown> | null, client?: Record<string, unknown> | null, db?: DrizzleDb) => Promise<unknown>;
117
+
118
+ /** RPC handler with per-function role requirement and/or RLS context opt-in */
115
119
  export interface RpcHandlerWithRole {
116
120
  fn: RpcHandler;
117
121
  /** Role required to call this RPC (e.g., 'admin', 'authenticated') */
118
- role: string;
122
+ role?: string;
123
+ /** Run this RPC inside a pgSettings transaction (SET LOCAL ROLE +
124
+ * request.jwt.claims) and pass that transaction as the handler's 4th
125
+ * argument. The handler MUST query through the passed transaction —
126
+ * queries through a captured outer db run on a different pooled
127
+ * connection without the settings, and on a single-connection pool
128
+ * (Lambda) they deadlock against the open transaction. */
129
+ settings?: boolean;
119
130
  }
120
131
 
121
132
  export interface HandlerOptions {
@@ -503,9 +514,19 @@ export function createHandler(db: DrizzleDb, schema: Schema, options?: HandlerOp
503
514
 
504
515
  const fn = isObjectForm ? entry.fn : entry;
505
516
  const body = method === 'POST' ? await request.json().catch(() => ({})) : {};
506
- const settings = options?.pgSettings?.(user, client);
507
517
  try {
508
- const result = await executeWithSettings(db, settings, () => fn(body, user, client));
518
+ // Plain RPCs dispatch directly — wrapping them in the settings
519
+ // transaction deadlocks single-connection pools (the fn's queries
520
+ // go through a captured db that waits on the connection the
521
+ // transaction holds). Only `settings: true` entries get the
522
+ // transaction, and they receive it as the 4th argument.
523
+ const result = isObjectForm && entry.settings
524
+ ? await executeWithSettings(
525
+ db,
526
+ options?.pgSettings?.(user, client),
527
+ (tx) => fn(body, user, client, tx),
528
+ )
529
+ : await fn(body, user, client);
509
530
  return json(result, 200);
510
531
  } catch (error) {
511
532
  if (error instanceof HandlerError) {