@query-farm/vgi 0.38.0 → 0.40.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
@@ -615,6 +615,37 @@ never return `null` for one: both read as a definitive answer. Return `null` onl
615
615
  when the store answered and the credential is unknown; throw `GrantRefusedError` to
616
616
  decline a grant.
617
617
 
618
+ ### Sealed grants: minted credentials that log their owner in
619
+
620
+ `issue_grant` mints a credential meant for unattended automation to present later
621
+ as an ordinary bearer. Configure **grant keys** and the worker both mints and
622
+ accepts them, with no storage and no code:
623
+
624
+ ```console
625
+ $ VGI_RPC_GRANT_KEYS="$(openssl rand -base64 32)" bun run scripts/serve.ts
626
+ # or: bun run scripts/serve.ts --grant-key "$(openssl rand -base64 32)"
627
+ ```
628
+
629
+ - `VGI_RPC_GRANT_KEYS`: comma-separated standard base64, exactly 32 bytes each. The
630
+ first mints; every key verifies. Rotate by adding the new key first and removing
631
+ the old one once its grants have expired. `--grant-key KEY` (repeatable) on
632
+ `serveVgiWorker`, or the `grantKeys` option (`GrantKeys.parse([...])`, required on
633
+ Cloudflare Workers, where there is no process environment) work too.
634
+ - Optional `VGI_RPC_GRANT_AUDIENCE` (bind grants to this deployment) and
635
+ `VGI_RPC_GRANT_MAX_TTL_SECONDS` (default 7 days).
636
+ - With keys set, HTTP hosts `issue_grant` (minted by the framework unless you supply
637
+ `mintGrant`) and the worker's HTTP authentication accepts `vgig1.` grants after
638
+ your own `authenticate`, as the grant's owner (domain `grant`). A caller
639
+ authenticated by a grant has no `auth_time`, so it cannot mint another grant.
640
+ - A worker that supplies `resolveToken` also has bearers your `authenticate` did not
641
+ accept resolved through it (domain `token`); an `AuthUnavailableError` from the hook
642
+ is a 503 with `Retry-After`, never a 401.
643
+ - Your `authenticate` must throw (not return anonymous) for a bearer it does not
644
+ recognise, or the chain ends before grants are tried.
645
+ - No key set, nothing changes. A malformed key stops the worker at startup.
646
+ - Sealed grants are not individually revocable: keep the lifetime short; removing a
647
+ key revokes every grant it minted.
648
+
618
649
  ## Runtimes & entry points
619
650
 
620
651
  The package ships a backend-agnostic Arrow facade and selects an implementation at
@@ -1,9 +1,9 @@
1
1
  import { type VgiBatch } from "../arrow/index.js";
2
2
  import type { RpcClient } from "@query-farm/vgi-rpc";
3
- import { type SchemaInfo, type TableInfo, type ViewInfo, type FunctionInfo, type MacroInfo, type MacroType, type CatalogAttachResult, type SchemaContentsInfo, type AttachOpaqueData, type TransactionOpaqueData, type CatalogInfo, type ScanFunctionResult } from "../catalog/interface.js";
3
+ import { type SchemaInfo, type TableInfo, type ViewInfo, type FunctionInfo, type MacroInfo, type MacroType, type CatalogAttachResult, type SchemaContentsInfo, type IndexInfo, type AttachOpaqueData, type TransactionOpaqueData, type CatalogInfo, type ScanFunctionResult } from "../catalog/interface.js";
4
4
  import { VgiClientError } from "./errors.js";
5
5
  export { VgiClientError };
6
- import type { VgiClientOptions, TableFunctionOptions, ScalarFunctionOptions, TableInOutFunctionOptions, OnCreateConflict, CatalogFunctionType, CatalogMacroType, CatalogAttachOptions, AttachOptionValue } from "./types.js";
6
+ import type { VgiClientOptions, TableFunctionOptions, ScalarFunctionOptions, TableInOutFunctionOptions, OnCreateConflict, CatalogFunctionType, CatalogMacroType, CatalogAttachOptions, AttachOptionValue, CatalogSnapshot, LoadCatalogOptions } from "./types.js";
7
7
  /**
8
8
  * High-level client for calling VGI worker functions and catalog API.
9
9
  *
@@ -103,6 +103,9 @@ export declare class VgiClient {
103
103
  * current the worker answers `not_modified` with no schemas. `etag` null
104
104
  * means the worker does not revalidate. Each schema's wire `path` is checked
105
105
  * against its decoded `SchemaInfo.path`.
106
+ *
107
+ * This is the raw RPC. To enumerate a catalog, use `loadCatalog`, which
108
+ * checks the capability, revalidates and falls back to the per-schema RPCs.
106
109
  */
107
110
  catalogContents(attachOpaqueData: AttachOpaqueData, ifNoneMatch?: string | null): Promise<{
108
111
  catalog_version: number;
@@ -110,6 +113,30 @@ export declare class VgiClient {
110
113
  not_modified: boolean;
111
114
  schemas: SchemaContentsInfo[];
112
115
  }>;
116
+ /**
117
+ * Enumerate the whole catalog: every schema with all of its tables, views,
118
+ * scalar/aggregate/table functions, scalar/table macros and indexes.
119
+ *
120
+ * Honors the attach result's `supports_catalog_contents` the way the DuckDB
121
+ * extension does:
122
+ *
123
+ * - advertised (and `useCatalogContents` not false, no transaction): one
124
+ * `catalog_contents` call. With `previous.etag` it is conditional
125
+ * (`if_none_match`); `not_modified` returns `previous`'s contents.
126
+ * - not advertised: `catalog_schemas` + per-schema
127
+ * `catalog_schema_contents_*` calls -- `catalog_contents` is never sent.
128
+ * Kinds whose `estimated_object_count` is exactly 0 are skipped.
129
+ * - advertised but the call fails (or answers malformed, e.g. a
130
+ * `SchemaContents.path` that disagrees with its `SchemaInfo.path`): falls
131
+ * back to the per-schema calls and reports the failure in
132
+ * `fallback_error`.
133
+ *
134
+ * A full answer older than `previous` (a lagging replica) is retried once,
135
+ * then the per-schema path is used.
136
+ */
137
+ loadCatalog(attach: Pick<CatalogAttachResult, "attach_opaque_data" | "supports_catalog_contents">, opts?: LoadCatalogOptions): Promise<CatalogSnapshot>;
138
+ /** `loadCatalog` without `catalog_contents`: catalog_schemas + per-schema calls. */
139
+ private _loadCatalogPerSchema;
113
140
  /** Detach a previously-attached catalog. */
114
141
  catalogDetach(attachOpaqueData: AttachOpaqueData): Promise<void>;
115
142
  /**
@@ -153,6 +180,8 @@ export declare class VgiClient {
153
180
  schemaContentsViews(attachOpaqueData: AttachOpaqueData, path: string[] | string, transactionOpaqueData?: TransactionOpaqueData): Promise<ViewInfo[]>;
154
181
  /** List functions in a schema, filtered by type. */
155
182
  schemaContentsFunctions(attachOpaqueData: AttachOpaqueData, path: string[] | string, type: CatalogFunctionType, transactionOpaqueData?: TransactionOpaqueData): Promise<FunctionInfo[]>;
183
+ /** List indexes in a schema. */
184
+ schemaContentsIndexes(attachOpaqueData: AttachOpaqueData, path: string[] | string, transactionOpaqueData?: TransactionOpaqueData): Promise<IndexInfo[]>;
156
185
  /** Get a table by name, or null if not found. */
157
186
  tableGet(attachOpaqueData: AttachOpaqueData, schemaPath: string[] | string, name: string, transactionOpaqueData?: TransactionOpaqueData): Promise<TableInfo | null>;
158
187
  /** Create a new table. */
@@ -2,6 +2,7 @@ import type { VgiBatch } from "../arrow/index.js";
2
2
  import type { Arguments } from "../arguments/arguments.js";
3
3
  import type { AttachOptionValue } from "../catalog/attach-options.js";
4
4
  import type { BindResponse } from "../protocol/types.js";
5
+ import type { SchemaContentsInfo, TransactionOpaqueData } from "../catalog/interface.js";
5
6
  import { OrderByDirection, OrderByNullOrder } from "../protocol/types.js";
6
7
  export type { AttachOptionValue };
7
8
  export { OrderByDirection, OrderByNullOrder };
@@ -46,7 +47,7 @@ export interface CatalogAttachOptions {
46
47
  /** Conflict resolution strategy for create operations. */
47
48
  export type OnCreateConflict = "error" | "ignore" | "replace";
48
49
  /** DuckDB catalog function type filter (sent as uppercase wire values). */
49
- export type CatalogFunctionType = "SCALAR_FUNCTION" | "TABLE_FUNCTION";
50
+ export type CatalogFunctionType = "SCALAR_FUNCTION" | "AGGREGATE_FUNCTION" | "TABLE_FUNCTION";
50
51
  /** Macro type filter for schema contents listing (uppercase wire values). */
51
52
  export type CatalogMacroType = "SCALAR_MACRO" | "TABLE_MACRO";
52
53
  /** Options for constructing a VgiClient. */
@@ -136,3 +137,57 @@ export interface TableInOutFunctionOptions {
136
137
  */
137
138
  hasFinalize?: boolean;
138
139
  }
140
+ /**
141
+ * A whole-catalog snapshot returned by `VgiClient.loadCatalog`: every schema
142
+ * with all of its tables, views, functions, macros and indexes.
143
+ *
144
+ * Hold on to it and pass it back as `LoadCatalogOptions.previous` to
145
+ * revalidate: when it carries an `etag` the client sends that as
146
+ * `if_none_match`, and a `not_modified` answer returns this snapshot's
147
+ * contents unchanged.
148
+ */
149
+ export interface CatalogSnapshot {
150
+ /**
151
+ * Version the snapshot was taken at, as reported by `catalog_contents`.
152
+ * `null` when it was assembled from the per-schema RPCs (which carry no
153
+ * version); 0 means the worker does not track versions.
154
+ */
155
+ catalog_version: number | null;
156
+ /** Validator to revalidate with; null = the worker does not revalidate. */
157
+ etag: string | null;
158
+ /**
159
+ * How the contents were obtained: one `catalog_contents` call, or
160
+ * `catalog_schemas` + per-schema `catalog_schema_contents_*` calls.
161
+ */
162
+ source: "catalog_contents" | "per_schema";
163
+ /** True when this load was a `not_modified` revalidation of `previous`. */
164
+ not_modified: boolean;
165
+ /**
166
+ * Set when the catalog advertised `catalog_contents` but the call failed
167
+ * (or returned a malformed answer) and the client fell back to the
168
+ * per-schema RPCs: the failure's message.
169
+ */
170
+ fallback_error: string | null;
171
+ /** One entry per schema, parents before children. */
172
+ schemas: SchemaContentsInfo[];
173
+ }
174
+ /** Options for `VgiClient.loadCatalog`. */
175
+ export interface LoadCatalogOptions {
176
+ /**
177
+ * A snapshot from an earlier load of the same attach. Its `etag` (if any)
178
+ * is sent as `if_none_match`; a `not_modified` answer keeps its contents.
179
+ */
180
+ previous?: CatalogSnapshot | null;
181
+ /**
182
+ * Set false to never call `catalog_contents` (the client-side equivalent of
183
+ * DuckDB's `SET vgi_catalog_contents = false`). Default true: it is used
184
+ * whenever the attach result advertised `supports_catalog_contents`.
185
+ */
186
+ useCatalogContents?: boolean;
187
+ /**
188
+ * Enumerate inside this transaction. `catalog_contents` returns only the
189
+ * committed catalog (it takes no transaction), so a transaction-scoped load
190
+ * always uses the per-schema RPCs.
191
+ */
192
+ transactionOpaqueData?: TransactionOpaqueData;
193
+ }
@@ -2388,21 +2388,23 @@ function normalizeValue(raw, type) {
2388
2388
  return out;
2389
2389
  }
2390
2390
  if (isMap(type)) {
2391
+ const entries = type.children[0].type;
2392
+ const valueType = entries.children[1].type;
2391
2393
  const out = {};
2394
+ const put = (k, v) => {
2395
+ out[String(k)] = normalizeValue(v, valueType);
2396
+ };
2392
2397
  if (raw[Symbol.iterator]) {
2393
2398
  for (const entry of raw) {
2394
2399
  if (Array.isArray(entry)) {
2395
- out[String(entry[0])] = String(entry[1] ?? "");
2400
+ put(entry[0], entry[1]);
2396
2401
  } else if (entry && typeof entry === "object") {
2397
- const k = entry.key ?? entry[0];
2398
- const v = entry.value ?? entry[1];
2399
- out[String(k)] = v == null ? "" : String(v);
2402
+ put(entry.key ?? entry[0], entry.value ?? entry[1]);
2400
2403
  }
2401
2404
  }
2402
2405
  } else if (typeof raw === "object") {
2403
- for (const [k, v] of Object.entries(raw)) {
2404
- out[String(k)] = v == null ? "" : String(v);
2405
- }
2406
+ for (const [k, v] of Object.entries(raw))
2407
+ put(k, v);
2406
2408
  }
2407
2409
  return out;
2408
2410
  }
@@ -3973,6 +3975,84 @@ class VgiClient {
3973
3975
  schemas
3974
3976
  };
3975
3977
  }
3978
+ async loadCatalog(attach, opts) {
3979
+ const attachOpaqueData = attach.attach_opaque_data;
3980
+ const previous = opts?.previous ?? null;
3981
+ const useContents = attach.supports_catalog_contents === true && opts?.useCatalogContents !== false && opts?.transactionOpaqueData == null;
3982
+ if (!useContents) {
3983
+ return this._loadCatalogPerSchema(attachOpaqueData, opts?.transactionOpaqueData, null);
3984
+ }
3985
+ let fallbackError = null;
3986
+ const ifNoneMatch = previous?.etag ?? null;
3987
+ const knownVersion = previous?.catalog_version ?? 0;
3988
+ for (let attempt = 0;attempt < 2; attempt++) {
3989
+ let response;
3990
+ try {
3991
+ response = await this.catalogContents(attachOpaqueData, ifNoneMatch);
3992
+ } catch (err) {
3993
+ fallbackError = err instanceof Error ? err.message : String(err);
3994
+ break;
3995
+ }
3996
+ if (response.not_modified) {
3997
+ if (previous == null || ifNoneMatch == null) {
3998
+ fallbackError = "catalog_contents answered not_modified to an unconditional request";
3999
+ break;
4000
+ }
4001
+ return {
4002
+ ...previous,
4003
+ catalog_version: response.catalog_version !== 0 ? response.catalog_version : previous.catalog_version,
4004
+ etag: response.etag ?? ifNoneMatch,
4005
+ not_modified: true,
4006
+ fallback_error: null
4007
+ };
4008
+ }
4009
+ if (response.catalog_version !== 0 && knownVersion !== 0 && response.catalog_version < knownVersion) {
4010
+ fallbackError = `catalog_contents snapshot version ${response.catalog_version} is older than the known version ${knownVersion}`;
4011
+ continue;
4012
+ }
4013
+ return {
4014
+ catalog_version: response.catalog_version,
4015
+ etag: response.etag,
4016
+ source: "catalog_contents",
4017
+ not_modified: false,
4018
+ fallback_error: null,
4019
+ schemas: response.schemas
4020
+ };
4021
+ }
4022
+ return this._loadCatalogPerSchema(attachOpaqueData, undefined, fallbackError);
4023
+ }
4024
+ async _loadCatalogPerSchema(attachOpaqueData, transactionOpaqueData, fallbackError) {
4025
+ const schemas = await this.schemas(attachOpaqueData, transactionOpaqueData);
4026
+ const out = [];
4027
+ for (const schema of schemas) {
4028
+ const counts = schema.estimated_object_count ?? {};
4029
+ const path = schema.path;
4030
+ const has = (kind) => {
4031
+ const n = counts[kind];
4032
+ return n == null || Number(n) !== 0;
4033
+ };
4034
+ const t = transactionOpaqueData;
4035
+ out.push({
4036
+ schema,
4037
+ tables: has("table") ? await this.schemaContentsTables(attachOpaqueData, path, t) : [],
4038
+ views: has("view") ? await this.schemaContentsViews(attachOpaqueData, path, t) : [],
4039
+ scalar_functions: has("scalar_function") ? await this.schemaContentsFunctions(attachOpaqueData, path, "SCALAR_FUNCTION", t) : [],
4040
+ aggregate_functions: has("aggregate_function") ? await this.schemaContentsFunctions(attachOpaqueData, path, "AGGREGATE_FUNCTION", t) : [],
4041
+ table_functions: has("table_function") ? await this.schemaContentsFunctions(attachOpaqueData, path, "TABLE_FUNCTION", t) : [],
4042
+ scalar_macros: has("macro") ? await this.schemaContentsMacros(attachOpaqueData, path, "SCALAR_MACRO", t) : [],
4043
+ table_macros: has("macro") ? await this.schemaContentsMacros(attachOpaqueData, path, "TABLE_MACRO", t) : [],
4044
+ indexes: has("index") ? await this.schemaContentsIndexes(attachOpaqueData, path, t) : []
4045
+ });
4046
+ }
4047
+ return {
4048
+ catalog_version: null,
4049
+ etag: null,
4050
+ source: "per_schema",
4051
+ not_modified: false,
4052
+ fallback_error: fallbackError,
4053
+ schemas: out
4054
+ };
4055
+ }
3976
4056
  async catalogDetach(attachOpaqueData) {
3977
4057
  await this.rpc.call("catalog_detach", { attach_opaque_data: attachOpaqueData });
3978
4058
  }
@@ -4111,6 +4191,17 @@ class VgiClient {
4111
4191
  const inner = unwrapResult(result);
4112
4192
  return deserializeInfoList(inner.items, decodeFunctionInfo);
4113
4193
  }
4194
+ async schemaContentsIndexes(attachOpaqueData, path, transactionOpaqueData) {
4195
+ const result = await this.rpc.call("catalog_schema_contents_indexes", {
4196
+ attach_opaque_data: attachOpaqueData,
4197
+ path: normalizeSchemaPath(path),
4198
+ transaction_opaque_data: transactionOpaqueData ?? null
4199
+ });
4200
+ if (!result)
4201
+ return [];
4202
+ const inner = unwrapResult(result);
4203
+ return deserializeInfoList(inner.items, decodeIndexInfo);
4204
+ }
4114
4205
  async tableGet(attachOpaqueData, schemaPath, name, transactionOpaqueData) {
4115
4206
  const result = await this.rpc.call("catalog_table_get", {
4116
4207
  attach_opaque_data: attachOpaqueData,
@@ -4549,4 +4640,4 @@ export {
4549
4640
  typeSignature
4550
4641
  };
4551
4642
 
4552
- //# debugId=AD4326E5920F408164756E2164756E21
4643
+ //# debugId=A700197CD6EACFA764756E2164756E21