@everystack/api 0.2.10 → 0.2.11
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 +7 -7
- package/src/handler/index.ts +30 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/api",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "PostgREST-compatible HTTP handler + typed fetch client for Drizzle ORM",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -30,11 +30,6 @@
|
|
|
30
30
|
"default": "./src/client/index.ts"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"test": "jest",
|
|
35
|
-
"build": "tsc --build",
|
|
36
|
-
"lint": "tsc --noEmit"
|
|
37
|
-
},
|
|
38
33
|
"peerDependencies": {
|
|
39
34
|
"drizzle-orm": "0.41.0"
|
|
40
35
|
},
|
|
@@ -45,5 +40,10 @@
|
|
|
45
40
|
"jest": "29.7.0",
|
|
46
41
|
"ts-jest": "29.4.9",
|
|
47
42
|
"typescript": "5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "jest",
|
|
46
|
+
"build": "tsc --build",
|
|
47
|
+
"lint": "tsc --noEmit"
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|
package/src/handler/index.ts
CHANGED
|
@@ -177,11 +177,27 @@ export interface RpcHandlerWithRole {
|
|
|
177
177
|
* connection without the settings, and on a single-connection pool
|
|
178
178
|
* (Lambda) they deadlock against the open transaction. */
|
|
179
179
|
settings?: boolean;
|
|
180
|
+
/** Route this RPC's ANONYMOUS calls to `options.readDb` (the read replica)
|
|
181
|
+
* when one is configured. Authenticated callers stay on the primary
|
|
182
|
+
* (read-your-writes). Only meaningful with `settings: true` — plain RPCs
|
|
183
|
+
* own their DB access through what they close over, so there is nothing
|
|
184
|
+
* for the handler to route (use createReadDb() in the fn instead). The
|
|
185
|
+
* fn must not write: on a real replica a write fails with "cannot
|
|
186
|
+
* execute … in a read-only transaction". */
|
|
187
|
+
readOnly?: boolean;
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
export interface HandlerOptions {
|
|
183
191
|
auth?: AuthConfig;
|
|
184
192
|
rpc?: Record<string, RpcHandler | RpcHandlerWithRole>;
|
|
193
|
+
/** Read-replica connection (e.g. createReadDb from @everystack/server/db).
|
|
194
|
+
* When provided, ANONYMOUS GET table reads execute against it; writes,
|
|
195
|
+
* authenticated requests, and RPCs stay on the primary `db`. RPCs opt in
|
|
196
|
+
* per-function with `readOnly: true`. Anonymous-only routing is what makes
|
|
197
|
+
* replication lag safe: authenticated users never read the replica, so
|
|
198
|
+
* read-your-writes holds trivially. Omitted → behavior identical to a
|
|
199
|
+
* single-connection handler. See docs/plans/read-replica-routing.md. */
|
|
200
|
+
readDb?: DrizzleDb;
|
|
185
201
|
/** Relation definitions for resource embedding: { [sourceTable]: { [relationName]: RelationConfig } } */
|
|
186
202
|
relations?: Record<string, Record<string, RelationConfig>>;
|
|
187
203
|
/** Base path to strip from request URL (e.g., '/api' when mounted at /api/[...path]) */
|
|
@@ -605,9 +621,15 @@ export function createHandler(db: DrizzleDb, schema: Schema, options?: HandlerOp
|
|
|
605
621
|
// go through a captured db that waits on the connection the
|
|
606
622
|
// transaction holds). Only `settings: true` entries get the
|
|
607
623
|
// transaction, and they receive it as the 4th argument.
|
|
624
|
+
// Read routing: a readOnly-marked settings RPC serves ANONYMOUS
|
|
625
|
+
// callers from the replica. Authenticated callers stay on the
|
|
626
|
+
// primary (read-your-writes).
|
|
627
|
+
const rpcDb = isObjectForm && entry.readOnly && !user && options?.readDb
|
|
628
|
+
? options.readDb
|
|
629
|
+
: db;
|
|
608
630
|
const result = isObjectForm && entry.settings
|
|
609
631
|
? await executeWithSettings(
|
|
610
|
-
|
|
632
|
+
rpcDb,
|
|
611
633
|
options?.pgSettings?.(user, client),
|
|
612
634
|
(tx) => fn(body, user, client, tx, ctx),
|
|
613
635
|
)
|
|
@@ -715,8 +737,14 @@ export function createHandler(db: DrizzleDb, schema: Schema, options?: HandlerOp
|
|
|
715
737
|
}
|
|
716
738
|
}
|
|
717
739
|
|
|
740
|
+
// Read routing: anonymous GETs serve from the replica when one is
|
|
741
|
+
// configured. `user` is resolved (incl. opportunistic auth) above, so an
|
|
742
|
+
// authenticated GET stays on the primary — read-your-writes holds because
|
|
743
|
+
// only anonymous traffic can ever reach the replica. Writes never route.
|
|
744
|
+
const routedDb = method === 'GET' && !user && options?.readDb ? options.readDb : db;
|
|
745
|
+
|
|
718
746
|
try {
|
|
719
|
-
const response = await executeWithSettings(
|
|
747
|
+
const response = await executeWithSettings(routedDb, settings, async (dbOrTx) => {
|
|
720
748
|
if (method === 'GET') {
|
|
721
749
|
const cacheControl = getCacheControl(options);
|
|
722
750
|
return handleGet(dbOrTx, schema, table, tableName, url.searchParams, request, options, cacheControl);
|