@dalgo/dalgo2postgrest 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DALgo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,405 @@
1
- # Temporary Holding Version
1
+ # DALgo adapter for PostgREST
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@dalgo/dalgo2postgrest` is a deliberately bounded, dependency-free TypeScript
4
+ adapter from [`@dalgo/core`](https://github.com/dal-go/dalgo-js) to exposed
5
+ PostgREST tables or updatable views. It uses the platform `fetch` API only. It
6
+ does not call PostgreSQL directly, an administration endpoint, RPC, or resource
7
+ embedding. A read-only discovery surface uses the OpenAPI document PostgREST
8
+ already serves for the request role.
9
+
10
+ ## Security and browser boundary
11
+
12
+ Pass an HTTPS `baseUrl` without credentials and a `headers` provider. HTTPS is
13
+ the default and the only option for a hosted origin. Plain HTTP is accepted
14
+ only for `localhost`, `127.0.0.1` or `[::1]`, and only with the explicit
15
+ `allowInsecureLoopbackHttp: true` development option.
16
+
17
+ The header provider runs for every HTTP request, allowing the application to
18
+ obtain a fresh, narrowly scoped Bearer token or cookie from its own
19
+ authenticated token broker. The adapter neither retains nor logs those values.
20
+ Never put database credentials or access tokens in a URL, source control,
21
+ analytics, browser storage, or an error report. Do not install a service-role
22
+ or other administration key in a browser.
23
+
24
+ Supabase is PostgREST. A browser example uses its publishable/anon `apikey`
25
+ plus a **least-privilege user bearer** from the application's own token broker:
26
+
27
+ ```ts
28
+ const db = new PostgrestDatabase({
29
+ baseUrl: "https://project.supabase.co/rest/v1",
30
+ headers: async () => ({
31
+ apikey: publicAnonKey, // publishable key, not a service-role key
32
+ authorization: `Bearer ${await getFreshUserAccessToken()}`,
33
+ }),
34
+ });
35
+ ```
36
+
37
+ PostgREST authenticates the JWT and performs authorization in the database.
38
+ Configure token audience, role grants, row-level security, CORS
39
+ origin/method/header allowlists, cookie policy, and proxy boundaries for the
40
+ deployment. Direct browser access is an application decision; this package
41
+ cannot make an overly broad CORS policy or a token with the wrong audience safe.
42
+ The package source is browser-safe and imports no Node built-in.
43
+
44
+ ## Install
45
+
46
+ `@dalgo/core` and `@dalgo/adapter-runtime` are peers. This package is not yet
47
+ published to npm. First-wave consumers pin a reviewed commit of this monorepo as
48
+ a pnpm Git subdirectory dependency, the same pattern used by
49
+ `@dalgo/sqlite-wasm`.
50
+
51
+ Each `github:` subdirectory dependency is fetched as its own workspace copy, and
52
+ only that copy is prepared. The package's `prepack` therefore builds
53
+ `@dalgo/adapter-runtime` from the same commit before type-checking itself; a
54
+ separate prebuilt runtime tarball or a local workspace link is not required.
55
+ Substitute the full 40-character SHA of the reviewed merged commit for
56
+ `<merged-sha>` in every place below:
57
+
58
+ ```jsonc
59
+ // package.json
60
+ "@dalgo/core": "github:dal-go/dalgo-js#99165e9e1a39c32abee2b39d6395677843bc3559",
61
+ "@dalgo/adapter-runtime": "github:dal-go/dalgo-http-adapters#<merged-sha>&path:/packages/adapter-runtime",
62
+ "@dalgo/dalgo2postgrest": "github:dal-go/dalgo-http-adapters#<merged-sha>&path:/packages/postgrest"
63
+ ```
64
+
65
+ ```yaml
66
+ # pnpm-workspace.yaml: pnpm blocks build scripts until each exact package is approved
67
+ allowBuilds:
68
+ '@dalgo/core@https://codeload.github.com/dal-go/dalgo-js/tar.gz/99165e9e1a39c32abee2b39d6395677843bc3559': true
69
+ '@dalgo/adapter-runtime@https://codeload.github.com/dal-go/dalgo-http-adapters/tar.gz/<merged-sha>#path:/packages/adapter-runtime': true
70
+ '@dalgo/dalgo2postgrest@https://codeload.github.com/dal-go/dalgo-http-adapters/tar.gz/<merged-sha>#path:/packages/postgrest': true
71
+ ```
72
+
73
+ If an entry is missing, pnpm names the exact key it needs; approve only the
74
+ reviewed dependency, never a wildcard. From this repository,
75
+ `pnpm check:git-subdir-consumer` installs these three packages from a temporary
76
+ Git repository in a clean consumer and requires a strict typecheck, a dynamic
77
+ import of every entry point, exactly one `@dalgo/core` `0.7.4` instance, no
78
+ `@dal-go/dalgo`, and no local links. Run the identical check against a pushed
79
+ commit with
80
+ `DALGO_ADAPTER_GIT_SUBDIR_REF=github:dal-go/dalgo-http-adapters#<sha> pnpm check:git-subdir-consumer`.
81
+
82
+ The `@dalgo/core` peer range is `>=0.7.4 <0.9.0`; the workspace tests against the
83
+ exact `0.7.4` commit the web UI pins, so the browser contains one `Key`/query
84
+ identity. From the repository root, `pnpm install --frozen-lockfile` and
85
+ `pnpm --filter @dalgo/dalgo2postgrest check` run the workspace gate.
86
+
87
+ ## Minimal setup
88
+
89
+ ```ts
90
+ import { collection } from "@dalgo/core";
91
+ import { PostgrestDatabase } from "@dalgo/dalgo2postgrest";
92
+
93
+ const db = new PostgrestDatabase({
94
+ baseUrl: "https://api.example.com/rest/v1",
95
+ headers: async () => ({ authorization: `Bearer ${await getFreshPostgrestToken()}` }),
96
+ });
97
+
98
+ const items = collection<{ title: string; done: boolean }>("items");
99
+ await db.insert(items.key("milk"), { title: "Buy milk", done: false });
100
+ ```
101
+
102
+ The type-checked version is [examples/basic.ts](examples/basic.ts). This maps
103
+ the top-level DALgo collection `items` to the exposed `items` relation and its
104
+ DALgo key ID to the relation's `id` column. Use `relationName` and `idColumn`
105
+ when the schema uses different names. The relation must have a primary key or
106
+ unique constraint suitable for PostgREST upsert, and the supplied identity must
107
+ have only the needed database permissions.
108
+
109
+ Use `schema` (or its `profile` alias) to select a PostgREST profile explicitly.
110
+ The adapter sends `Accept-Profile` on reads and `Content-Profile` on writes.
111
+ Profile selection never changes the configured `idColumn` or `relationName`
112
+ mapping, and no profile is guessed from OpenAPI metadata.
113
+
114
+ ## Discovery and connection preflight
115
+
116
+ `discover()` reads the root OpenAPI document with the same refreshed headers and
117
+ caller cancellation as every other request. It returns only what the document
118
+ actually declares, bounded by relation count, fields per relation, document
119
+ bytes and local `$ref` depth. Remote `$ref` documents are never fetched.
120
+
121
+ ```ts
122
+ import { POSTGREST_READ_CAPABILITIES, PostgrestDatabase } from "@dalgo/dalgo2postgrest";
123
+
124
+ const db = new PostgrestDatabase({ baseUrl, headers });
125
+ const status = await db.testConnection(controller.signal);
126
+
127
+ if (status.ok) {
128
+ const discovery = await db.discover(controller.signal);
129
+ for (const relation of discovery.relations) {
130
+ console.log(relation.name, relation.fields, relation.primaryKey, relation.adapterQueryable);
131
+ }
132
+ }
133
+
134
+ const discoveryCapability = POSTGREST_READ_CAPABILITIES.query.discovery;
135
+ ```
136
+
137
+ `discover()` never returns an empty relation list as if it were proof of an
138
+ empty database. A denied identity, a disabled/missing OpenAPI endpoint, a
139
+ malformed document, and a role with no visible relations are reported as
140
+ `status: "unavailable"` with an explicit `reason` (`unauthorized`, `forbidden`,
141
+ `metadata-disabled`, `metadata-malformed`, `request-failed`, or
142
+ `no-relations`). `testConnection()` maps that to a UI-safe status and does not
143
+ echo a response body, header, token, or transport error.
144
+
145
+ Field types, formats, array item types, enums and primary-key markers appear
146
+ only where the OpenAPI document declares them. A field whose type is expressed
147
+ by an unresolvable/remote `$ref` is marked `typeStatus: "unavailable"`. The
148
+ configured `idColumn` is shown per relation as `idColumn`, and
149
+ `adapterQueryable` says whether this adapter can address the relation with the
150
+ configured mapping. This is read-only discovery, not an administration API.
151
+
152
+ Primary-key markers are collected from the complete declared property set, not
153
+ from the bounded `fields` list, so a truncated field projection cannot turn a
154
+ composite key into a false single-column key. `idColumn.uniqueStatus` separates
155
+ three cases: `declared-primary-key` (the complete declared key is exactly that
156
+ one column), `configured-contract` (the role's metadata declares no key, so the
157
+ adapter relies on the caller's configured uniqueness contract), and
158
+ `not-unique` (a different or composite key is declared, so the configured
159
+ column alone cannot address one row, a stable keyset boundary, or a per-row
160
+ write). `adapterQueryable` is `false` for `not-unique`; `readable` is separate
161
+ provider authorization and never proves uniqueness or pagination stability.
162
+
163
+ ## Supported DALgo surface
164
+
165
+ | DALgo operation | PostgREST mapping |
166
+ | --- | --- |
167
+ | `get` | Bounded `GET /relation?id=eq.value&limit=1`; an empty result is a missing snapshot. |
168
+ | `getMany` | Ordered, bounded, bounded-parallel point reads. |
169
+ | `insert` | `POST /relation` with an adapter-owned ID column; HTTP 409 becomes `AlreadyExistsError`. |
170
+ | `set` | Single-row `PUT /relation?id=eq.value` with the complete payload; exactly one returned row is required. |
171
+ | `update` | `PATCH /relation?id=eq.value` with strict `max-affected=1`; an empty returned representation becomes `NotFoundError`. |
172
+ | `delete` | `DELETE /relation?id=eq.value` with strict `max-affected=1`; an empty returned representation is an idempotent missing delete. |
173
+ | `query` | Legacy direct top-level relation `GET` with PostgREST filters, ordering, bounded `limit`, and `offset`; DALgo cursors are still refused. |
174
+ | `queryWithSignal` | The same query with caller cancellation, one end-to-end deadline, and optional opaque keyset cursor continuation. |
175
+
176
+ The adapter-owned ID column is removed before codec decoding. Consequently,
177
+ codec-encoded record data must not contain that column. Query records construct
178
+ top-level DALgo keys from its value. This is the **native-rows** storage mode:
179
+ the adapter reads existing relations and never rewrites them into a DALgo
180
+ envelope. Numeric, `null`, boolean and string values remain as PostgREST
181
+ returned them; no projection is silently applied.
182
+
183
+ ## Cursor pagination
184
+
185
+ `queryWithSignal()` returns `nextCursor` for every non-empty page, including a
186
+ page shorter than the requested limit. A short page is not proof of
187
+ end-of-data: PostgREST's `db-max-rows` (or a proxy) can cap every response below
188
+ the requested `limit`. Pass the token back as `startAfter(...)` until a request
189
+ returns an empty page. The cursor is opaque base64url and binds:
190
+ endpoint, DALgo collection and exposed relation, configured profile and
191
+ `idColumn`, filters, effective order, `select=*`, page size, and an optional
192
+ non-secret `identityScope`. A cursor from another endpoint, relation, query,
193
+ order, page size or identity scope is rejected with `PostgrestCursorError`
194
+ (which is also an `UnsupportedError`) before HTTP.
195
+
196
+ ```ts
197
+ const pageSize = 25; // adapter page, not a semantic total
198
+ let cursor: string | undefined;
199
+ do {
200
+ const builder = items.query().where("done", "==", false).orderBy("priority").limit(pageSize);
201
+ const page = await db.queryWithSignal(
202
+ cursor === undefined ? builder.build() : builder.startAfter(cursor).build(),
203
+ controller.signal,
204
+ );
205
+ render(page.records);
206
+ cursor = page.nextCursor?.values[0] as string | undefined;
207
+ } while (cursor !== undefined);
208
+ ```
209
+
210
+ The effective order always ends in the configured primary key, so a stable page
211
+ has a deterministic tie-breaker even when the user order is not unique. The
212
+ adapter uses keyset continuation (`or=(...)`), not a full scan and not
213
+ `Prefer: count=exact`. Every non-empty page, short or full, may advertise a
214
+ possible next page; the follow-up request that finds no rows ends pagination
215
+ honestly with `{ records: [] }` and no cursor. A consumer that needs a bounded
216
+ total stops after its own budget or the empty page.
217
+
218
+ Legacy `query()` keeps its one-request `limit`/`offset` contract and never
219
+ returns a cursor; a short legacy page is returned as-is.
220
+
221
+ Two limits are intentionally different:
222
+
223
+ - **Adapter page limit**: `maxQueryLimit` (default 100, hard maximum 1,000) is
224
+ the largest number of rows in one provider request/page.
225
+ - **DataTug logical total/query limit**: a consumer/UI concern. The adapter does
226
+ not compute, cache, infer or guarantee a semantic total. Use explicit
227
+ continuation until the empty page, or the consumer's own bounded total.
228
+
229
+ Offsets remain available for both legacy `query()` and `queryWithSignal()` and
230
+ are bounded by `maxQueryOffset`. Cursor and offset cannot be combined. Cursor
231
+ pages are bounded by `maxQueryPages`; a cursor that exceeds that bound is
232
+ refused explicitly rather than silently restarting. Cursor metadata is bounded
233
+ by `maxCursorBytes`.
234
+
235
+ ## Cancellation and capability admission
236
+
237
+ ```ts
238
+ import { QueryCancelledError } from "@dalgo/adapter-runtime";
239
+ import { POSTGREST_READ_CAPABILITIES, PostgrestDatabase } from "@dalgo/dalgo2postgrest";
240
+ import { collection } from "@dalgo/core";
241
+
242
+ const capabilities = POSTGREST_READ_CAPABILITIES;
243
+ const built = collection<{ done: boolean }>("items").query().orderBy("done").limit(25).build();
244
+ if (capabilities.query.sort.status === "unsupported" && built.orders.length > 0) {
245
+ throw new Error("PostgREST does not support ordering");
246
+ }
247
+
248
+ const controller = new AbortController();
249
+ try {
250
+ const page = await db.queryWithSignal(built, controller.signal);
251
+ return page;
252
+ } catch (error) {
253
+ if (error instanceof QueryCancelledError) {
254
+ // error.reason is "caller" or "deadline"; no token or abort reason leaks.
255
+ return undefined;
256
+ }
257
+ throw error;
258
+ }
259
+ ```
260
+
261
+ The caller signal covers header acquisition, `fetch`, response-body reading and
262
+ cursor preparation. The adapter combines it with the configured `timeoutMs`
263
+ (15 seconds by default, maximum 120) as one end-to-end deadline. A pre-aborted
264
+ signal does no header or HTTP work; a late response body is cancelled and a late
265
+ result is never returned. `queryWithSignal` rejects with `QueryCancelledError`
266
+ for caller abort/deadline and with the existing redacted
267
+ `PostgrestRequestError` for header/fetch/body transport failures. Deliberate
268
+ query/unsupported errors are raised before any request and keep their messages.
269
+ Legacy `query` keeps its interface and success behaviour.
270
+
271
+ ### Capability rows
272
+
273
+ | Feature | Status | Bound / reason |
274
+ | --- | --- | --- |
275
+ | read/get, read/getMany | native | `getMany` max 100 keys, 8 parallel reads by default |
276
+ | projection | unsupported | core `StructuredQuery` has no projection; `select=*` is always used and no field is dropped |
277
+ | filter | native | simple column identifiers or `DOCUMENT_ID`; all DALgo operators; null only for `==`/`!=` |
278
+ | sort | native | `asc`/`desc` on simple column identifiers; `queryWithSignal` adds the primary-key tie-breaker |
279
+ | pagination | native | bounded `limit`/`offset`; opaque keyset cursor in `queryWithSignal`; legacy `query` refuses cursors |
280
+ | joins, aggregation, grouping, subquery, fulltext, timeseries | unsupported | refused before HTTP |
281
+ | native query | unsupported | no raw PostgREST parameters, embedding or RPC |
282
+ | discovery | native | read-only bounded OpenAPI; role visibility and denied/disabled metadata are explicit |
283
+ | bounds | enforced | query limit 100/1000, offset 100k/10M, pages 10k/1M, cursor 8KiB/64KiB, request/response 1 MiB/16 MiB, discovery 512KiB/16MiB, 100/10k relations, 200/10k fields, depth 4/16 |
284
+
285
+ The exported constant is `POSTGREST_READ_CAPABILITIES`; `storage` is
286
+ `"native-rows"` and `bounds` contains the constructor default and hard maximum
287
+ for each numeric limit, including the PostgREST-specific discovery and cursor
288
+ bounds.
289
+
290
+ ## Intentional limitations
291
+
292
+ - DALgo callback transactions are rejected. PostgREST executes each HTTP
293
+ request in its own database transaction, so this package cannot honestly
294
+ provide an atomic, retryable callback across requests.
295
+ - Nested DALgo keys, nested-collection queries, and collection-group queries
296
+ are rejected. A relation is one top-level DALgo collection, not a hierarchy.
297
+ - Projection cannot be expressed through the current core `StructuredQuery`;
298
+ the adapter does not pretend it is supported. Discovery exposes field
299
+ metadata, while row reads always select `*`.
300
+ - Supported filters are equality/inequality, comparisons, `in`, `not-in`, and
301
+ PostgreSQL-array containment/overlap; relation and field names are limited to
302
+ simple SQL identifiers. No resource embedding, JSON paths, full-text search,
303
+ aggregates, RPC, or server management API is exposed. A string used with a
304
+ single-value operator (`eq`, `neq`, `lt`, `lte`, `gt`, `gte`) is sent raw,
305
+ because PostgREST compares that operand literally and would include the
306
+ double quotes otherwise. Strings in a membership list (`in`, `not-in`) or an
307
+ array literal (`cs`, `ov`) are quoted and escaped, which is the documented
308
+ reserved-character context.
309
+ - Reserved PostgREST control parameters (`select`, `columns`, `on_conflict`,
310
+ `order`, `limit`, `offset`, `and`, `or`) are quoted when they are used as
311
+ filter keys, order fields, or the configured `idColumn`, using PostgREST's
312
+ documented identifier quoting. Appending them unquoted would let PostgREST
313
+ consume the predicate as its own control and silently drop it. Names that
314
+ merely resemble a control (`order_id`, `limiters`, `ordinary`) stay unquoted
315
+ and keep working.
316
+ - JSON numbers are checked against the raw response text before `JSON.parse`.
317
+ A number literal is accepted only when the decimal value it denotes is the
318
+ exact decimal value the adapter would serialize back. A high-precision
319
+ decimal such as `0.10000000000000001`, an exponent token that underflows to
320
+ zero (`1e-400`) or overflows to a non-finite value (`1e400`), `-0`, and any
321
+ integer (including an integer-valued decimal or exponent) outside the
322
+ lossless JavaScript safe range are refused with a redacted error before any
323
+ record or cursor is emitted. Canonical decimals such as `0.1`,
324
+ `0.30000000000000004` or `1.0`/`1e0` remain supported because their
325
+ serialized literal preserves the same decimal value. Outgoing filter,
326
+ record and cursor values are checked the same way. This prevents an ID such
327
+ as `9007199254740993` from becoming `9007199254740992`, and a keyset boundary
328
+ such as `0.10000000000000001` from being sent as `0.1`, which could repeat
329
+ the same actual numeric row.
330
+ - `set` is full replacement, not merge-upsert: the supplied DALgo value is the
331
+ complete replacement record (apart from the adapter-owned ID). Fields omitted
332
+ from it are not sent for merge. PostgREST must expose a relation for which its
333
+ documented single-row `PUT` semantics are available.
334
+ - `update` and `delete` ask PostgREST for `handling=strict, max-affected=1`
335
+ and verify returned cardinality. `set` verifies one returned `PUT` row;
336
+ PostgREST documents `max-affected` for PATCH and DELETE, not PUT.
337
+ - PostgREST errors may contain database details and row values. Response bodies
338
+ are cancelled and discarded for non-2xx replies; `PostgrestHttpError` exposes
339
+ only the HTTP status. Header-provider, fetch, redirect, timeout, body-stream,
340
+ and JSON failures become a generic `PostgrestRequestError`, never their
341
+ original text. Inserts request `return=minimal` and cancel any unexpected
342
+ response body; keyed writes read a bounded representation to prove their
343
+ cardinality. Cancellation is best-effort and never awaited, so a hostile or
344
+ broken stream cannot outlive the request deadline.
345
+ - Cursor pagination assumes the configured primary-key column is non-null and
346
+ unique. Discovery reports whether that is declared (`declared-primary-key`)
347
+ or relies on the configured contract (`configured-contract`). When metadata
348
+ declares a different or composite key, `uniqueStatus` is `not-unique` and
349
+ `adapterQueryable` is false: the adapter does not promise a unique key or
350
+ stable cursor boundary. A relation without a declared key can still be read
351
+ by key and offset, but that is the caller's uniqueness contract.
352
+ - Discovery reflects the OpenAPI document for the current role at the time of
353
+ the request. It can lag a schema-cache reload, and it is not a grant of read
354
+ access to every advertised relation.
355
+
356
+ ## Official PostgREST references
357
+
358
+ - [Tables and Views](https://docs.postgrest.org/en/stable/references/api/tables_views.html)
359
+ - [Pagination and Count](https://docs.postgrest.org/en/stable/references/api/pagination_count.html)
360
+ - [OpenAPI](https://docs.postgrest.org/en/stable/references/api/openapi.html)
361
+ - [Prefer Header](https://docs.postgrest.org/en/stable/references/api/preferences.html)
362
+ - [Schemas](https://docs.postgrest.org/en/stable/references/api/schemas.html)
363
+ - [Authentication](https://docs.postgrest.org/en/stable/references/auth.html)
364
+ - [Errors](https://docs.postgrest.org/en/stable/references/errors.html)
365
+
366
+ ## Verification
367
+
368
+ `pnpm check` runs ESLint, deterministic Vitest HTTP-contract tests, declaration
369
+ build, and type checks of the minimal example. The unit suite injects `fetch`
370
+ for protocol/error/bound coverage. `test/node-http-transport.test.ts` is a Node
371
+ HTTP contract test only: Node does not enforce browser CORS and the test sets
372
+ `Origin` manually, so it does not prove browser behaviour.
373
+
374
+ `pnpm test:browser` owns a second disposable Compose project named
375
+ `datatug-postgrest-ts-r2-<run>` and a unique loopback static origin. It first
376
+ builds the workspace dependency outputs (`@dalgo/adapter-runtime` among them),
377
+ so it works from a clean source checkout, then builds the fixture page with
378
+ esbuild, launches real headless Chromium through Playwright, and has the
379
+ browser bundle use the real `PostgrestDatabase` and
380
+ global `fetch` against that real PostgREST service. The service allows exactly
381
+ the static page origin. The suite proves successful discovery, native rows and
382
+ multi-page continuation across server-capped short pages, a redacted bad
383
+ credential, an actual browser CORS refusal from a different origin, and caller
384
+ abort. It does not intercept any PostgREST response. It removes only its own
385
+ Compose project and fixture directory.
386
+
387
+ `pnpm test:real` owns one disposable Docker Compose project named
388
+ `datatug-postgrest-ts-<run>`, uses cached `postgrest/postgrest:v16.5` and
389
+ `postgres:17` images, random loopback host ports, a synthetic JWT role, a
390
+ read-only role, native seeded rows, `db-max-rows=2` below the requested page
391
+ size, and exact-origin CORS. It runs the real PostgREST discovery, short-page
392
+ cursor journey, changed/foreign cursor, bad-auth, read-only denial, no-relations
393
+ and controlled-header tests, then removes only its own project and temporary
394
+ files. Neither real-service suite proves a hosted Supabase deployment, its RLS
395
+ policies, a gateway, or DataTug's actual connection form and UI journey; those
396
+ remain separate acceptance gates for the owning application.
397
+
398
+ `pnpm check:packed-consumer` (repository root) additionally packs the published
399
+ artifacts into a fresh directory outside the repository, installs only those
400
+ tarballs plus the exact modern `@dalgo/core` commit, and type-checks and runs the
401
+ packed declarations with no `@dal-go/dalgo` package available.
402
+
403
+ ## License
404
+
405
+ MIT
@@ -0,0 +1,129 @@
1
+ import type { AdapterReadCapabilities, NumericCapabilityBound, QueryCapabilityBounds } from "@dalgo/adapter-runtime";
2
+ /** PostgREST-specific bounds in addition to the shared adapter capability shape. */
3
+ export interface PostgrestQueryBounds extends QueryCapabilityBounds {
4
+ readonly maxQueryOffset: NumericCapabilityBound;
5
+ readonly maxQueryPages: NumericCapabilityBound;
6
+ readonly maxDiscoveryRelations: NumericCapabilityBound;
7
+ readonly maxDiscoveryFieldsPerRelation: NumericCapabilityBound;
8
+ readonly maxDiscoveryBytes: NumericCapabilityBound;
9
+ readonly maxCursorBytes: NumericCapabilityBound;
10
+ readonly maxSchemaDepth: NumericCapabilityBound;
11
+ }
12
+ export interface PostgrestCapabilityDescriptor extends Omit<AdapterReadCapabilities, "bounds"> {
13
+ readonly bounds: PostgrestQueryBounds;
14
+ }
15
+ /**
16
+ * Honest read/query capabilities of the PostgREST adapter.
17
+ *
18
+ * It reads existing exposed relations and never rewrites rows into a DALgo
19
+ * envelope. Discovery is a bounded read of the OpenAPI document generated for
20
+ * the request role; it is not an administration API and not a promise that the
21
+ * role can read every advertised relation. Core `StructuredQuery` has no
22
+ * projection field, so projection is explicitly unsupported rather than
23
+ * silently ignored.
24
+ */
25
+ export declare const POSTGREST_READ_CAPABILITIES: {
26
+ readonly adapter: "@dalgo/dalgo2postgrest";
27
+ readonly storage: "native-rows";
28
+ readonly read: {
29
+ readonly get: true;
30
+ readonly getMany: true;
31
+ readonly maxGetManyKeys: {
32
+ readonly default: 100;
33
+ readonly maximum: 1000;
34
+ };
35
+ readonly maxParallelReads: {
36
+ readonly default: 8;
37
+ readonly maximum: 1000;
38
+ };
39
+ };
40
+ readonly query: {
41
+ readonly projection: {
42
+ readonly status: "unsupported";
43
+ readonly detail: "core StructuredQuery has no projection; every record is selected with select=* and no field is silently dropped";
44
+ };
45
+ readonly filter: {
46
+ readonly status: "native";
47
+ readonly detail: "simple column identifiers or DOCUMENT_ID; null only for equality and inequality; PostgREST control names select/columns/on_conflict/order/limit/offset/and/or are quoted as field keys so the predicate is preserved";
48
+ };
49
+ readonly sort: {
50
+ readonly status: "native";
51
+ readonly detail: "ascending or descending on simple column identifiers; queryWithSignal adds the configured primary key as a deterministic tie-breaker";
52
+ };
53
+ readonly pagination: {
54
+ readonly status: "native";
55
+ readonly detail: "limit/offset in query and queryWithSignal, plus an adapter-bound opaque keyset cursor for every non-empty page in queryWithSignal; a short page is not end-of-data; legacy query refuses cursors; no count=exact or full scan";
56
+ };
57
+ readonly joins: {
58
+ readonly status: "unsupported";
59
+ };
60
+ readonly aggregation: {
61
+ readonly status: "unsupported";
62
+ };
63
+ readonly grouping: {
64
+ readonly status: "unsupported";
65
+ };
66
+ readonly subquery: {
67
+ readonly status: "unsupported";
68
+ };
69
+ readonly fulltext: {
70
+ readonly status: "unsupported";
71
+ };
72
+ readonly timeseries: {
73
+ readonly status: "unsupported";
74
+ };
75
+ readonly native: {
76
+ readonly status: "unsupported";
77
+ readonly detail: "the adapter does not expose raw PostgREST query parameters, embedding, RPC or administration endpoints";
78
+ };
79
+ readonly discovery: {
80
+ readonly status: "native";
81
+ readonly detail: "read-only bounded OpenAPI discovery of relations, declared field types and declared primary keys for the request role; denied/disabled/empty metadata is explicit";
82
+ };
83
+ };
84
+ readonly bounds: {
85
+ readonly maxQueryLimit: {
86
+ readonly default: 100;
87
+ readonly maximum: 1000;
88
+ };
89
+ readonly maxQueryOffset: {
90
+ readonly default: 100000;
91
+ readonly maximum: 10000000;
92
+ };
93
+ readonly maxQueryPages: {
94
+ readonly default: 10000;
95
+ readonly maximum: 1000000;
96
+ };
97
+ readonly maxRequestBytes: {
98
+ readonly default: 1048576;
99
+ readonly maximum: 16777216;
100
+ };
101
+ readonly maxResponseBytes: {
102
+ readonly default: 1048576;
103
+ readonly maximum: 16777216;
104
+ };
105
+ readonly maxDiscoveryRelations: {
106
+ readonly default: 100;
107
+ readonly maximum: 10000;
108
+ };
109
+ readonly maxDiscoveryFieldsPerRelation: {
110
+ readonly default: 200;
111
+ readonly maximum: 10000;
112
+ };
113
+ readonly maxDiscoveryBytes: {
114
+ readonly default: 524288;
115
+ readonly maximum: 16777216;
116
+ };
117
+ readonly maxCursorBytes: {
118
+ readonly default: 8192;
119
+ readonly maximum: 65536;
120
+ };
121
+ readonly maxSchemaDepth: {
122
+ readonly default: 4;
123
+ readonly maximum: 16;
124
+ };
125
+ readonly filterOperators: readonly ["==", "!=", "<", "<=", ">", ">=", "in", "not-in", "array-contains", "array-contains-any"];
126
+ readonly orderDirections: readonly ["asc", "desc"];
127
+ };
128
+ };
129
+ export type PostgrestReadCapabilities = typeof POSTGREST_READ_CAPABILITIES;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Honest read/query capabilities of the PostgREST adapter.
3
+ *
4
+ * It reads existing exposed relations and never rewrites rows into a DALgo
5
+ * envelope. Discovery is a bounded read of the OpenAPI document generated for
6
+ * the request role; it is not an administration API and not a promise that the
7
+ * role can read every advertised relation. Core `StructuredQuery` has no
8
+ * projection field, so projection is explicitly unsupported rather than
9
+ * silently ignored.
10
+ */
11
+ export const POSTGREST_READ_CAPABILITIES = {
12
+ adapter: "@dalgo/dalgo2postgrest",
13
+ storage: "native-rows",
14
+ read: {
15
+ get: true,
16
+ getMany: true,
17
+ maxGetManyKeys: { default: 100, maximum: 1_000 },
18
+ maxParallelReads: { default: 8, maximum: 1_000 },
19
+ },
20
+ query: {
21
+ projection: {
22
+ status: "unsupported",
23
+ detail: "core StructuredQuery has no projection; every record is selected with select=* and no field is silently dropped",
24
+ },
25
+ filter: {
26
+ status: "native",
27
+ detail: "simple column identifiers or DOCUMENT_ID; null only for equality and inequality; PostgREST control names select/columns/on_conflict/order/limit/offset/and/or are quoted as field keys so the predicate is preserved",
28
+ },
29
+ sort: {
30
+ status: "native",
31
+ detail: "ascending or descending on simple column identifiers; queryWithSignal adds the configured primary key as a deterministic tie-breaker",
32
+ },
33
+ pagination: {
34
+ status: "native",
35
+ detail: "limit/offset in query and queryWithSignal, plus an adapter-bound opaque keyset cursor for every non-empty page in queryWithSignal; a short page is not end-of-data; legacy query refuses cursors; no count=exact or full scan",
36
+ },
37
+ joins: { status: "unsupported" },
38
+ aggregation: { status: "unsupported" },
39
+ grouping: { status: "unsupported" },
40
+ subquery: { status: "unsupported" },
41
+ fulltext: { status: "unsupported" },
42
+ timeseries: { status: "unsupported" },
43
+ native: {
44
+ status: "unsupported",
45
+ detail: "the adapter does not expose raw PostgREST query parameters, embedding, RPC or administration endpoints",
46
+ },
47
+ discovery: {
48
+ status: "native",
49
+ detail: "read-only bounded OpenAPI discovery of relations, declared field types and declared primary keys for the request role; denied/disabled/empty metadata is explicit",
50
+ },
51
+ },
52
+ bounds: {
53
+ maxQueryLimit: { default: 100, maximum: 1_000 },
54
+ maxQueryOffset: { default: 100_000, maximum: 10_000_000 },
55
+ maxQueryPages: { default: 10_000, maximum: 1_000_000 },
56
+ maxRequestBytes: { default: 1_048_576, maximum: 16_777_216 },
57
+ maxResponseBytes: { default: 1_048_576, maximum: 16_777_216 },
58
+ maxDiscoveryRelations: { default: 100, maximum: 10_000 },
59
+ maxDiscoveryFieldsPerRelation: { default: 200, maximum: 10_000 },
60
+ maxDiscoveryBytes: { default: 524_288, maximum: 16_777_216 },
61
+ maxCursorBytes: { default: 8_192, maximum: 65_536 },
62
+ maxSchemaDepth: { default: 4, maximum: 16 },
63
+ filterOperators: [
64
+ "==",
65
+ "!=",
66
+ "<",
67
+ "<=",
68
+ ">",
69
+ ">=",
70
+ "in",
71
+ "not-in",
72
+ "array-contains",
73
+ "array-contains-any",
74
+ ],
75
+ orderDirections: ["asc", "desc"],
76
+ },
77
+ };
78
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,OAAO,EAAE,wBAAwB;IACjC,OAAO,EAAE,aAAa;IACtB,IAAI,EAAE;QACJ,GAAG,EAAE,IAAI;QACT,OAAO,EAAE,IAAI;QACb,cAAc,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;QAChD,gBAAgB,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE;KACjD;IACD,KAAK,EAAE;QACL,UAAU,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,iHAAiH;SAC1H;QACD,MAAM,EAAE;YACN,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,sNAAsN;SAC/N;QACD,IAAI,EAAE;YACJ,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,sIAAsI;SAC/I;QACD,UAAU,EAAE;YACV,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,+NAA+N;SACxO;QACD,KAAK,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QAChC,WAAW,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QACtC,QAAQ,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QACnC,QAAQ,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QACnC,QAAQ,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QACnC,UAAU,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;QACrC,MAAM,EAAE;YACN,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,wGAAwG;SACjH;QACD,SAAS,EAAE;YACT,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,mKAAmK;SAC5K;KACF;IACD,MAAM,EAAE;QACN,aAAa,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;QAC/C,cAAc,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE;QACzD,aAAa,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE;QACtD,eAAe,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE;QAC5D,gBAAgB,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE;QAC7D,qBAAqB,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE;QACxD,6BAA6B,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE;QAChE,iBAAiB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE;QAC5D,cAAc,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE;QACnD,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3C,eAAe,EAAE;YACf,IAAI;YACJ,IAAI;YACJ,GAAG;YACH,IAAI;YACJ,GAAG;YACH,IAAI;YACJ,IAAI;YACJ,QAAQ;YACR,gBAAgB;YAChB,oBAAoB;SACrB;QACD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC;KACjC;CAC+C,CAAC"}