@dalgo/dalgo2postgrest 0.0.0-bootstrap.0 → 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 +1 -2
- package/README.md +404 -4
- package/dist/capabilities.d.ts +129 -0
- package/dist/capabilities.js +78 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cursor.d.ts +60 -0
- package/dist/cursor.js +261 -0
- package/dist/cursor.js.map +1 -0
- package/dist/database.d.ts +95 -0
- package/dist/database.js +730 -0
- package/dist/database.js.map +1 -0
- package/dist/discovery.d.ts +111 -0
- package/dist/discovery.js +235 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fields.d.ts +20 -0
- package/dist/fields.js +32 -0
- package/dist/fields.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +25 -0
- package/dist/json.js +114 -0
- package/dist/json.js.map +1 -0
- package/package.json +50 -7
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 DALgo contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
package/README.md
CHANGED
|
@@ -1,5 +1,405 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DALgo adapter for PostgREST
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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"}
|