@query-farm/vgi 0.15.0 → 0.18.0
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/README.md +53 -0
- package/dist/cache-control.d.ts +90 -0
- package/dist/catalog/descriptors.d.ts +13 -7
- package/dist/client-entry.js +3862 -1
- package/dist/client-entry.js.map +34 -3
- package/dist/functions/table.d.ts +12 -0
- package/dist/generated/vgi-client.d.ts +1 -1
- package/dist/http/fetch.d.ts +35 -0
- package/dist/index.core.d.ts +1 -0
- package/dist/index.js +225 -11
- package/dist/index.js.map +11 -9
- package/dist/serve-entry.d.ts +84 -0
- package/dist/serve-entry.js +6428 -0
- package/dist/serve-entry.js.map +62 -0
- package/dist/worker-cf-entry.d.ts +4 -22
- package/dist/worker-cf-entry.js +466 -34
- package/dist/worker-cf-entry.js.map +12 -9
- package/package.json +18 -6
- package/src/cache-control.ts +133 -0
- package/src/catalog/descriptors.ts +13 -7
- package/src/catalog/read-only.ts +22 -14
- package/src/functions/table.ts +25 -1
- package/src/generated/vgi-client.ts +2 -2
- package/src/generated/vgi-protocol-schemas.ts +2 -2
- package/src/http/fetch.ts +80 -0
- package/src/index.core.ts +21 -0
- package/src/serve-entry.ts +233 -0
- package/src/worker-cf-entry.ts +7 -51
package/README.md
CHANGED
|
@@ -441,6 +441,59 @@ A worker serves the same functions over any of:
|
|
|
441
441
|
|
|
442
442
|
Transport handling lives in [`@query-farm/vgi-rpc`](https://www.npmjs.com/package/@query-farm/vgi-rpc).
|
|
443
443
|
|
|
444
|
+
### Serving a worker over HTTP
|
|
445
|
+
|
|
446
|
+
Don't hand-roll the HTTP wiring. `@query-farm/vgi/serve` assembles the protocol, the
|
|
447
|
+
signed state-token key, CORS, and the standardized landing surface, then binds the port:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
// scripts/serve.ts
|
|
451
|
+
import { serveVgiWorker } from "@query-farm/vgi/serve";
|
|
452
|
+
import { makeWorkerParts } from "../src/parts.js";
|
|
453
|
+
|
|
454
|
+
const { registry, catalogInterface } = makeWorkerParts();
|
|
455
|
+
|
|
456
|
+
serveVgiWorker({
|
|
457
|
+
name: "ishares",
|
|
458
|
+
doc: "iShares (BlackRock) US fund data.",
|
|
459
|
+
version: "0.1.0",
|
|
460
|
+
repositoryUrl: "https://github.com/Query-farm/vgi-etf-ishares",
|
|
461
|
+
registry,
|
|
462
|
+
catalogInterface,
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
```console
|
|
467
|
+
$ PORT=8787 bun run scripts/serve.ts
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Mounted at the origin root, so DuckDB attaches the bare URL:
|
|
471
|
+
|
|
472
|
+
```sql
|
|
473
|
+
ATTACH 'ishares' AS ishares (TYPE vgi, LOCATION 'http://localhost:8787');
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
It also serves `GET /` (landing page), `GET /describe.json` (catalog contract), and
|
|
477
|
+
`GET /health`.
|
|
478
|
+
|
|
479
|
+
| Variable | Default | Meaning |
|
|
480
|
+
| ----------------- | ------- | ----------------------------------------------------------- |
|
|
481
|
+
| `PORT` | `8787` | Listen port. `0` binds an ephemeral port. |
|
|
482
|
+
| `VGI_SIGNING_KEY` | random | State-token HMAC key, **64 hex chars** (`openssl rand -hex 32`). |
|
|
483
|
+
| `VGI_TOKEN_TTL` | `3600` | State-token lifetime in seconds. |
|
|
484
|
+
| `CORS_ORIGINS` | `*` | Allowed origins. Pass `corsOrigins: null` to disable CORS. |
|
|
485
|
+
|
|
486
|
+
Every variable has an explicit option that takes precedence over it.
|
|
487
|
+
|
|
488
|
+
With no `VGI_SIGNING_KEY`, a random key is generated and a warning is printed: state
|
|
489
|
+
tokens then stop validating after a restart, and never validate across two instances
|
|
490
|
+
behind a load balancer. Set it for any real deployment. A key that isn't exactly 64 hex
|
|
491
|
+
characters is rejected rather than silently truncated.
|
|
492
|
+
|
|
493
|
+
To mount the VGI routes inside a server you already own, use `createVgiWorkerFetch`,
|
|
494
|
+
which returns the `fetch` handler without binding a port. On Cloudflare Workers use
|
|
495
|
+
`createVgiFetch` from `@query-farm/vgi/worker-cf`; both share one implementation.
|
|
496
|
+
|
|
444
497
|
## Runtimes & entry points
|
|
445
498
|
|
|
446
499
|
The package ships a backend-agnostic Arrow facade and selects an implementation at
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result-cache control metadata (`vgi.cache.*`).
|
|
3
|
+
*
|
|
4
|
+
* A table function can advertise that its result is cacheable by the client
|
|
5
|
+
* (the DuckDB extension) by attaching `vgi.cache.*` metadata to the **first**
|
|
6
|
+
* data batch it emits. The vocabulary mirrors HTTP caching (RFC 9111/9110): a
|
|
7
|
+
* freshness lifetime (`ttl` / `expires`), a reuse `scope`, validators
|
|
8
|
+
* (ETag / Last-Modified) for conditional revalidation, and stale-serving grace
|
|
9
|
+
* windows.
|
|
10
|
+
*
|
|
11
|
+
* The key strings are the single source of truth shared with the C++ extension
|
|
12
|
+
* (which reads them by string). {@link cacheControlMetadata} renders a
|
|
13
|
+
* {@link CacheControl} to the `Map<string, string>` of `vgi.cache.*` keys that
|
|
14
|
+
* rides on batch metadata:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* out.emit(firstBatch, cacheControlMetadata({ ttl: 300 }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Pass `extra` to merge in metadata a function already emits per batch (for
|
|
21
|
+
* example `vgi_batch_index`); the rendered cache keys win on collision.
|
|
22
|
+
*
|
|
23
|
+
* Booleans render as `"1"` and are omitted when false; timestamps are RFC 3339
|
|
24
|
+
* UTC strings; durations are whole seconds.
|
|
25
|
+
*
|
|
26
|
+
* Ports `vgi/cache_control.py` from vgi-python.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CACHE_TTL_KEY = "vgi.cache.ttl";
|
|
29
|
+
export declare const CACHE_EXPIRES_KEY = "vgi.cache.expires";
|
|
30
|
+
export declare const CACHE_NO_STORE_KEY = "vgi.cache.no_store";
|
|
31
|
+
export declare const CACHE_SCOPE_KEY = "vgi.cache.scope";
|
|
32
|
+
export declare const CACHE_ETAG_KEY = "vgi.cache.etag";
|
|
33
|
+
export declare const CACHE_LAST_MODIFIED_KEY = "vgi.cache.last_modified";
|
|
34
|
+
export declare const CACHE_REVALIDATABLE_KEY = "vgi.cache.revalidatable";
|
|
35
|
+
export declare const CACHE_STALE_WHILE_REVALIDATE_KEY = "vgi.cache.stale_while_revalidate";
|
|
36
|
+
export declare const CACHE_STALE_IF_ERROR_KEY = "vgi.cache.stale_if_error";
|
|
37
|
+
export declare const CACHE_NOT_MODIFIED_KEY = "vgi.cache.not_modified";
|
|
38
|
+
export declare const CACHE_IF_NONE_MATCH_KEY = "vgi.cache.if_none_match";
|
|
39
|
+
export declare const CACHE_IF_MODIFIED_SINCE_KEY = "vgi.cache.if_modified_since";
|
|
40
|
+
export declare const CACHE_SCOPE_CATALOG = "catalog";
|
|
41
|
+
export declare const CACHE_SCOPE_TRANSACTION = "transaction";
|
|
42
|
+
/** Reuse scope for a cacheable result. */
|
|
43
|
+
export type CacheScope = typeof CACHE_SCOPE_CATALOG | typeof CACHE_SCOPE_TRANSACTION;
|
|
44
|
+
/**
|
|
45
|
+
* Cacheability advertised by a table function on its first result batch.
|
|
46
|
+
*
|
|
47
|
+
* Presence of `ttl` **or** `expires` is what makes a result cacheable;
|
|
48
|
+
* `noStore` overrides any freshness key.
|
|
49
|
+
*/
|
|
50
|
+
export interface CacheControl {
|
|
51
|
+
/** Freshness lifetime in whole seconds, relative to full-result receipt
|
|
52
|
+
* (skew-immune; wins over `expires`). */
|
|
53
|
+
ttl?: number;
|
|
54
|
+
/** Absolute RFC 3339 UTC deadline. Lifetime is `expires - now` at receipt. */
|
|
55
|
+
expires?: string;
|
|
56
|
+
/** Reuse scope — `"catalog"` (default; reusable across transactions within
|
|
57
|
+
* the calling catalog identity) or `"transaction"` (reused only within the
|
|
58
|
+
* same transaction). */
|
|
59
|
+
scope?: CacheScope;
|
|
60
|
+
/** Explicit "never cache"; overrides any freshness key. */
|
|
61
|
+
noStore?: boolean;
|
|
62
|
+
/** Strong validator (opaque quoted string) for conditional revalidation. */
|
|
63
|
+
etag?: string;
|
|
64
|
+
/** Weaker RFC 3339 UTC validator; fallback when no ETag. */
|
|
65
|
+
lastModified?: string;
|
|
66
|
+
/** The worker can check freshness cheaply without recomputing; gates whether
|
|
67
|
+
* the client ever sends a conditional request. */
|
|
68
|
+
revalidatable?: boolean;
|
|
69
|
+
/** Grace window (seconds) to serve stale immediately while revalidating in
|
|
70
|
+
* the background. */
|
|
71
|
+
staleWhileRevalidate?: number;
|
|
72
|
+
/** Grace window (seconds) to serve stale if a revalidation RPC fails. */
|
|
73
|
+
staleIfError?: number;
|
|
74
|
+
/** 304-equivalent — set on a 0-row batch in reply to a conditional request to
|
|
75
|
+
* assert the client's stored payload is still fresh (the client reuses it
|
|
76
|
+
* instead of re-streaming). */
|
|
77
|
+
notModified?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Render a {@link CacheControl} to the `vgi.cache.*` batch-metadata map.
|
|
81
|
+
*
|
|
82
|
+
* `scope` is always emitted so the client never has to infer the default.
|
|
83
|
+
* Unset optional fields are omitted; false booleans are omitted. Entries from
|
|
84
|
+
* `extra` are written first, so a rendered cache key wins on collision.
|
|
85
|
+
*
|
|
86
|
+
* Throws when `scope` is unrecognized or a duration is negative — a silently
|
|
87
|
+
* ignored advertisement is far harder to debug than a stack trace at the
|
|
88
|
+
* offending `emit()`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function cacheControlMetadata(cc: CacheControl, extra?: Map<string, string>): Map<string, string>;
|
|
@@ -53,18 +53,24 @@ export interface TableDescriptor {
|
|
|
53
53
|
*/
|
|
54
54
|
statisticsCacheMaxAgeSeconds?: number | null;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* Required WHERE-filter groups in conjunctive normal form — an AND (outer
|
|
57
|
+
* list) of OR-groups (inner lists) of dotted-path column references that MUST
|
|
58
|
+
* appear in a WHERE expression for any scan of this table. A group is
|
|
59
|
+
* satisfied when any one of its paths has a filter; every group must be
|
|
60
|
+
* satisfied. So `[["accession_number"], ["ticker", "cik"]]` means
|
|
61
|
+
* "accession_number AND one of (ticker, cik)"; a single-path group
|
|
62
|
+
* `[["country"]]` is a plain mandatory filter. Paths are top-level names
|
|
63
|
+
* (`"country"`) or struct subfields (`"bbox.xmin"`, `"nested.outer.inner"`).
|
|
64
|
+
* Empty/undefined (default) means no enforcement — the zero-cost fast path
|
|
65
|
+
* for every existing table.
|
|
60
66
|
*
|
|
61
67
|
* Satisfaction is prefix-based: a present filter on a shorter path satisfies
|
|
62
68
|
* any required path it is a prefix of. So a whole-struct filter on `bbox`
|
|
63
69
|
* satisfies all of `bbox.xmin` / `.xmax` / `.ymin` / `.ymax`. The VGI DuckDB
|
|
64
|
-
* extension's optimizer pass consults this
|
|
65
|
-
* `BinderException` listing any unsatisfied
|
|
70
|
+
* extension's optimizer pass consults this at bind time and throws a
|
|
71
|
+
* `BinderException` listing any unsatisfied groups.
|
|
66
72
|
*/
|
|
67
|
-
|
|
73
|
+
requiredFilters?: string[][];
|
|
68
74
|
supportsTimeTravel?: boolean;
|
|
69
75
|
/**
|
|
70
76
|
* Inline cardinality estimate/max surfaced through TableInfo. When set,
|