@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 +1 -1
- package/src/client/index.ts +23 -0
- package/src/handler/index.ts +28 -7
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
package/src/handler/index.ts
CHANGED
|
@@ -108,14 +108,25 @@ export interface CacheConfig {
|
|
|
108
108
|
private?: string;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** RPC handler — plain function (backward compatible)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|