@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 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
- * Dotted-path column references that MUST appear in a WHERE expression for
57
- * any scan of this table. Top-level names (`"country"`) or struct subfields
58
- * (`"bbox.xmin"`, `"nested.outer.inner"`). Empty/undefined (default) means
59
- * no enforcement — the zero-cost fast path for every existing table.
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 list at bind time and throws a
65
- * `BinderException` listing any unsatisfied paths.
70
+ * extension's optimizer pass consults this at bind time and throws a
71
+ * `BinderException` listing any unsatisfied groups.
66
72
  */
67
- requiredFieldFilterPaths?: string[];
73
+ requiredFilters?: string[][];
68
74
  supportsTimeTravel?: boolean;
69
75
  /**
70
76
  * Inline cardinality estimate/max surfaced through TableInfo. When set,