1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Redacted from "effect/Redacted";
5import type * as Scope from "effect/Scope";
6import * as FetchHttpClient from "effect/http/FetchHttpClient";
7import * as HttpClient from "effect/http/HttpClient";
8import type * as HttpClientError from "effect/http/HttpClientError";
9import * as HttpClientRequest from "effect/http/HttpClientRequest";
10import type * as HttpClientResponse from "effect/http/HttpClientResponse";
11import * as Binding from "../Binding.ts";
12import type { RuntimeContext } from "../RuntimeContext.ts";
13import {
14 backendEnvKey,
15 backendString,
16 bindBackendEnvironment,
17} from "./BackendConnection.ts";
18import type { DataApi } from "./DataApi.ts";
19
20export class DataApiRequestError extends Data.TaggedError(
21 "DataApiRequestError",
22)<{ message: string }> {}
23
24export interface QueryDataApiClient {
25 /** Bound public PostgREST endpoint. */
26 baseUrl: Effect.Effect<string, never, RuntimeContext>;
27 /** Execute a relative PostgREST request with this caller's token, never an admin key. */
28 execute: (
29 request: HttpClientRequest.HttpClientRequest,
30 token: Redacted.Redacted<string>,
31 ) => Effect.Effect<
32 HttpClientResponse.HttpClientResponse,
33 HttpClientError.HttpClientError | DataApiRequestError,
34 RuntimeContext | Scope.Scope
35 >;
36}
37
38/**
39 * Query Neon Data API (PostgREST) over HTTP from a Function, Worker, or Lambda
40 * using an explicitly supplied caller token. Supports reads, writes and database
41 * function calls; this binding does not provision database grants or RLS policies.
42 *
43 * This is a low-level Effect HTTP client, not a SQL driver or a fluent query
44 * builder. Browser applications can use Neon's `@neondatabase/neon-js` or
45 * `@neondatabase/postgrest-js` SDK directly with the public Data API URL.
46 *
47 * Relative URLs cannot redirect the authorization header to another origin.
48 * Requests do not follow redirects, and response bodies remain request-scoped.
49 *
50 * ### Forward end-user authorization
51 * **Example:** Query rows under the caller's RLS identity
52 * ```typescript
53 * const data = yield* Neon.QueryDataApi(dataApi);
54 * // In the request handler, after obtaining the caller's token:
55 * const response = yield* data.execute(HttpClientRequest.get("todos?select=*"), token);
56 * ```
57 *
58 * @binding
59 * @product Data API
60 */
61export interface QueryDataApi extends Binding.Service<
62 QueryDataApi,
63 "Neon.QueryDataApi",
64 (dataApi: DataApi) => Effect.Effect<QueryDataApiClient>
65> {}
66export const QueryDataApi = Binding.Service<QueryDataApi>("Neon.QueryDataApi");
67
68/** Data API transport, with the host and HTTP client encapsulated at initialization. */
69export const QueryDataApiHttp = Layer.effect(
70 QueryDataApi,
71 Effect.gen(function* () {
72 const http = yield* HttpClient.HttpClient;
73 return Effect.fn(function* (dataApi: DataApi) {
74 const key = backendEnvKey(dataApi.FQN, "DATA_API_URL");
75 yield* bindBackendEnvironment(`Neon.QueryDataApi:${dataApi.FQN}`, {
76 [key]: dataApi.url,
77 });
78 const baseUrl = backendString(key);
79 return {
80 baseUrl,
81 execute: Effect.fn(function* (
82 request: HttpClientRequest.HttpClientRequest,
83 token: Redacted.Redacted<string>,
84 ) {
85 if (!Redacted.value(token))
86 return yield* new DataApiRequestError({
87 message: "An end-user token is required",
88 });
89 const base = (yield* baseUrl).replace(/\/$/, "") + "/";
90 const target = yield* Effect.try({
91 try: () => new URL(request.url.replace(/^\/(?!\/)/, ""), base),
92 catch: () =>
93 new DataApiRequestError({
94 message: "Invalid relative Data API URL",
95 }),
96 });
97 const origin = yield* Effect.sync(() => new URL(base));
98 if (
99 target.origin !== origin.origin ||
100 !target.pathname.startsWith(origin.pathname) ||
101 target.username ||
102 target.password
103 ) {
104 return yield* new DataApiRequestError({
105 message: "Data API request must stay within its bound endpoint",
106 });
107 }
108 return yield* http
109 .execute(
110 request.pipe(
111 HttpClientRequest.setUrl(target.href),
112 HttpClientRequest.bearerToken(token),
113 ),
114 )
115 .pipe(
116 Effect.provideService(FetchHttpClient.RequestInit, {
117 redirect: "manual",
118 }),
119 );
120 }),
121 } satisfies QueryDataApiClient;
122 });
123 }),
124);