@dalgo/bigquery 0.0.0-stage → 0.2.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +472 -2
  3. package/dist/analytical/canonical.d.ts +10 -0
  4. package/dist/analytical/canonical.d.ts.map +1 -0
  5. package/dist/analytical/canonical.js +50 -0
  6. package/dist/analytical/client.d.ts +84 -0
  7. package/dist/analytical/client.d.ts.map +1 -0
  8. package/dist/analytical/client.js +730 -0
  9. package/dist/analytical/deadline.d.ts +13 -0
  10. package/dist/analytical/deadline.d.ts.map +1 -0
  11. package/dist/analytical/deadline.js +23 -0
  12. package/dist/analytical/google-identity.d.ts +40 -0
  13. package/dist/analytical/google-identity.d.ts.map +1 -0
  14. package/dist/analytical/google-identity.js +158 -0
  15. package/dist/analytical/ledger.d.ts +80 -0
  16. package/dist/analytical/ledger.d.ts.map +1 -0
  17. package/dist/analytical/ledger.js +89 -0
  18. package/dist/analytical/metadata-client.d.ts +66 -0
  19. package/dist/analytical/metadata-client.d.ts.map +1 -0
  20. package/dist/analytical/metadata-client.js +247 -0
  21. package/dist/analytical/metadata-harness.d.ts +28 -0
  22. package/dist/analytical/metadata-harness.d.ts.map +1 -0
  23. package/dist/analytical/metadata-harness.js +127 -0
  24. package/dist/analytical/metadata.d.ts +8 -0
  25. package/dist/analytical/metadata.d.ts.map +1 -0
  26. package/dist/analytical/metadata.js +155 -0
  27. package/dist/analytical/protocol.d.ts +157 -0
  28. package/dist/analytical/protocol.d.ts.map +1 -0
  29. package/dist/analytical/protocol.js +320 -0
  30. package/dist/analytical/public-metadata.d.ts +29 -0
  31. package/dist/analytical/public-metadata.d.ts.map +1 -0
  32. package/dist/analytical/public-metadata.js +70 -0
  33. package/dist/analytical/transport.d.ts +39 -0
  34. package/dist/analytical/transport.d.ts.map +1 -0
  35. package/dist/analytical/transport.js +224 -0
  36. package/dist/analytical/values.d.ts +18 -0
  37. package/dist/analytical/values.d.ts.map +1 -0
  38. package/dist/analytical/values.js +157 -0
  39. package/dist/analytical/wire.d.ts +26 -0
  40. package/dist/analytical/wire.d.ts.map +1 -0
  41. package/dist/analytical/wire.js +141 -0
  42. package/dist/analytical.d.ts +27 -0
  43. package/dist/analytical.d.ts.map +1 -0
  44. package/dist/analytical.js +14 -0
  45. package/dist/database.d.ts +35 -0
  46. package/dist/database.d.ts.map +1 -0
  47. package/dist/database.js +437 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +2 -0
  51. package/dist/sql.d.ts +29 -0
  52. package/dist/sql.d.ts.map +1 -0
  53. package/dist/sql.js +198 -0
  54. package/dist/types.d.ts +49 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +1 -0
  57. package/package.json +58 -4
@@ -0,0 +1,247 @@
1
+ import { object, same } from "./metadata.js";
2
+ import { bounds, cloneFrozen, exactKeys, identifier, integerText, nonempty, project, timestamp, validatePrincipal } from "./protocol.js";
3
+ import { abortable, realClock, Transport } from "./transport.js";
4
+ import { AnalyticalError, fail } from "./wire.js";
5
+ function source(value) {
6
+ exactKeys(value, ["sourceId", "sourceProject", "datasetId", "tableId"]);
7
+ nonempty(value.sourceId, 128);
8
+ project(value.sourceProject);
9
+ identifier(value.datasetId);
10
+ identifier(value.tableId);
11
+ }
12
+ function consent(value, expected) {
13
+ exactKeys(value, ["purpose", "ownerId", "consentId", "source", "principal", "selectedJobProject"]);
14
+ if (value.purpose !== "metadata-only")
15
+ fail("approval_required");
16
+ nonempty(value.ownerId);
17
+ nonempty(value.consentId);
18
+ source(value.source);
19
+ if (!same(value.source, expected))
20
+ fail("approval_changed");
21
+ validatePrincipal(value.principal);
22
+ if (value.principal.kind !== "google-user")
23
+ fail("auth_required");
24
+ project(value.selectedJobProject);
25
+ }
26
+ function wireText(value, maximum = 1024) {
27
+ if (typeof value !== "string" || value.length < 1 || value.length > maximum)
28
+ fail("malformed_wire");
29
+ }
30
+ function nativeSchema(value) {
31
+ const schema = object(value);
32
+ let count = 0;
33
+ const fields = (value, depth) => {
34
+ if (!Array.isArray(value) || value.length === 0)
35
+ fail("malformed_wire");
36
+ if (depth > 8)
37
+ fail("response_limit");
38
+ const names = new Set();
39
+ for (const raw of value) {
40
+ if (++count > 1024)
41
+ fail("response_limit");
42
+ const field = object(raw);
43
+ wireText(field.name, 300);
44
+ wireText(field.type, 64);
45
+ if (!/^[A-Z][A-Z0-9_]*$/u.test(field.type) || names.has(field.name))
46
+ fail("malformed_wire");
47
+ names.add(field.name);
48
+ if (field.mode !== undefined && (typeof field.mode !== "string" || !["NULLABLE", "REQUIRED", "REPEATED"].includes(field.mode)))
49
+ fail("malformed_wire");
50
+ if ((field.type === "RECORD" || field.type === "STRUCT") !== Object.hasOwn(field, "fields"))
51
+ fail("malformed_wire");
52
+ if (field.fields !== undefined)
53
+ fields(field.fields, depth + 1);
54
+ }
55
+ };
56
+ fields(schema.fields, 0);
57
+ }
58
+ function projection(raw, expected, table) {
59
+ const value = object(raw);
60
+ const referenceKey = table ? "tableReference" : "datasetReference";
61
+ const reference = object(value[referenceKey]);
62
+ exactKeys(reference, table ? ["projectId", "datasetId", "tableId"] : ["projectId", "datasetId"], [], "malformed_wire");
63
+ if (reference.projectId !== expected.sourceProject || reference.datasetId !== expected.datasetId || table && reference.tableId !== expected.tableId)
64
+ fail("source_changed");
65
+ // Dataset location is authoritative for this discovery; do not assume US.
66
+ if (!table || Object.hasOwn(value, "location"))
67
+ wireText(value.location, 64);
68
+ if (table) {
69
+ wireText(value.type, 64);
70
+ nativeSchema(value.schema);
71
+ }
72
+ for (const key of ["etag", "lastModifiedTime"]) {
73
+ if (!Object.hasOwn(value, key))
74
+ continue;
75
+ if (key === "lastModifiedTime")
76
+ integerText(value[key]);
77
+ else
78
+ wireText(value[key]);
79
+ }
80
+ const keys = table
81
+ ? [referenceKey, "type", "location", "etag", "lastModifiedTime", "schema", "timePartitioning", "rangePartitioning", "clustering", "requirePartitionFilter"]
82
+ : [referenceKey, "location", "etag", "lastModifiedTime"];
83
+ const result = {};
84
+ for (const key of keys)
85
+ if (Object.hasOwn(value, key))
86
+ result[key] = value[key];
87
+ return result;
88
+ }
89
+ /** No query, dry-run, row, ledger, export or cost-admission method exists here. */
90
+ export class BigQueryMetadataClient {
91
+ #sources;
92
+ #authorize;
93
+ #current;
94
+ #transport;
95
+ #clock;
96
+ #bounds;
97
+ #active = false;
98
+ constructor(config) {
99
+ exactKeys(config, ["sources", "provider", "authorizeMetadata", "currentMetadataBinding"], ["limits", "fetch", "clock"]);
100
+ if (!Array.isArray(config.sources) || config.sources.length < 1 || config.sources.length > 128 || typeof config.authorizeMetadata !== "function" || typeof config.currentMetadataBinding !== "function")
101
+ fail("invalid_input");
102
+ const sources = cloneFrozen(config.sources);
103
+ sources.forEach(source);
104
+ if (new Set(sources.map(value => value.sourceId)).size !== sources.length)
105
+ fail("invalid_input");
106
+ this.#sources = new Map(sources.map(value => [value.sourceId, value]));
107
+ this.#authorize = config.authorizeMetadata;
108
+ this.#current = config.currentMetadataBinding;
109
+ exactKeys(config.limits ?? {}, [], ["responseBytes", "totalResponseBytes", "wallMs", "httpMs"]);
110
+ this.#bounds = bounds({ responseBytes: 256 * 1024, totalResponseBytes: 1024 * 1024, wallMs: 30000, ...(config.limits ?? {}) });
111
+ this.#clock = config.clock ?? realClock;
112
+ this.#transport = new Transport(config.provider, config.fetch ?? ((url, init) => globalThis.fetch(url, init)), this.#clock);
113
+ }
114
+ async discover(sourceId, options = {}) {
115
+ nonempty(sourceId, 128);
116
+ const requested = this.#sources.get(sourceId);
117
+ if (requested === undefined)
118
+ fail("policy_denied");
119
+ exactKeys(options, [], ["signal", "deadline"]);
120
+ const signal = options.signal;
121
+ if (signal !== undefined && !(signal instanceof globalThis.AbortSignal))
122
+ fail("invalid_input");
123
+ const now = this.#clock.now();
124
+ if (!Number.isSafeInteger(now) || !Number.isSafeInteger(now + this.#bounds.wallMs) || options.deadline !== undefined && !Number.isSafeInteger(options.deadline))
125
+ fail("invalid_input");
126
+ const deadline = Math.min(now + this.#bounds.wallMs, options.deadline ?? Number.MAX_SAFE_INTEGER);
127
+ const controller = new AbortController();
128
+ const abort = () => controller.abort();
129
+ if (signal?.aborted || now >= deadline)
130
+ fail("local_stopped");
131
+ if (this.#active)
132
+ fail("policy_denied");
133
+ this.#active = true;
134
+ signal?.addEventListener("abort", abort, { once: true });
135
+ const timer = setTimeout(abort, deadline - now);
136
+ let result;
137
+ try {
138
+ // The overall abort race bounds preparation, retries and injected sleeps,
139
+ // even if a trusted integration ignores its signal.
140
+ result = await abortable(this.#discover(requested, deadline, controller.signal), controller.signal);
141
+ }
142
+ finally {
143
+ controller.abort();
144
+ this.#active = false;
145
+ clearTimeout(timer);
146
+ signal?.removeEventListener("abort", abort);
147
+ }
148
+ // Public delivery follows all async work and cleanup (including abort
149
+ // listeners). No await or other consumer callback follows the joint guard.
150
+ if (signal?.aborted || this.#clock.now() >= deadline)
151
+ fail("local_stopped");
152
+ this.#assertCurrent(result.consent);
153
+ return result;
154
+ }
155
+ #assertCurrent(prepared) {
156
+ let current;
157
+ try {
158
+ const snapshot = this.#current(prepared.source);
159
+ if (snapshot === undefined)
160
+ fail("approval_changed");
161
+ current = cloneFrozen(snapshot);
162
+ exactKeys(current, ["consent", "read", "expiresAt"]);
163
+ consent(current.consent, prepared.source);
164
+ if (!same(current.consent, prepared))
165
+ fail("approval_changed");
166
+ }
167
+ catch {
168
+ // No consumer error text, credentials or malformed/asynchronous snapshot
169
+ // may escape the trusted joint-binding boundary.
170
+ fail("approval_changed");
171
+ }
172
+ if (current.read !== true)
173
+ fail("scope_missing");
174
+ if (!Number.isSafeInteger(current.expiresAt) || current.expiresAt <= this.#clock.now())
175
+ fail("auth_expired");
176
+ }
177
+ async #attest(requested, signal) {
178
+ if (signal.aborted)
179
+ fail("local_stopped");
180
+ const controller = new AbortController();
181
+ const abort = () => controller.abort();
182
+ signal.addEventListener("abort", abort, { once: true });
183
+ const timer = setTimeout(abort, this.#bounds.httpMs);
184
+ try {
185
+ const prepared = cloneFrozen(await abortable(this.#authorize(requested, controller.signal), controller.signal));
186
+ if (controller.signal.aborted)
187
+ fail("local_stopped");
188
+ consent(prepared, requested);
189
+ return prepared;
190
+ }
191
+ catch (error) {
192
+ if (error instanceof AnalyticalError)
193
+ throw error;
194
+ return fail("approval_required");
195
+ }
196
+ finally {
197
+ controller.abort();
198
+ clearTimeout(timer);
199
+ signal.removeEventListener("abort", abort);
200
+ }
201
+ }
202
+ async #discover(requested, deadline, signal) {
203
+ const prepared = await this.#attest(requested, signal);
204
+ await this.#transport.verify(prepared.principal, Math.min(deadline, this.#clock.now() + this.#bounds.httpMs), signal);
205
+ let bytes = 0;
206
+ const scope = {
207
+ bounds: this.#bounds, principal: prepared.principal, executionDeadline: deadline, signal,
208
+ remaining: async () => this.#bounds.totalResponseBytes - bytes,
209
+ debit: async (count) => { bytes += count; if (bytes > this.#bounds.totalResponseBytes)
210
+ fail("response_limit"); },
211
+ beforeDispatch: async () => {
212
+ const current = await this.#attest(requested, signal);
213
+ if (!same(current, prepared))
214
+ fail("approval_changed");
215
+ // Consent preparation can disconnect/rotate the Google account while
216
+ // the transport awaits. Reverify before the physical dispatch.
217
+ await this.#transport.verify(prepared.principal, Math.min(deadline, this.#clock.now() + this.#bounds.httpMs), signal);
218
+ },
219
+ };
220
+ try {
221
+ const path = `projects/${requested.sourceProject}/datasets/${requested.datasetId}`;
222
+ // The existing synchronous dispatch hook runs after every async consent,
223
+ // identity and retry await, immediately before the physical fetch call.
224
+ const guard = () => this.#assertCurrent(prepared);
225
+ const dataset = projection(await this.#transport.call(scope, "GET", path, { datasetView: "METADATA" }, undefined, false, false, guard), requested, false);
226
+ const table = projection(await this.#transport.call(scope, "GET", `${path}/tables/${requested.tableId}`, { view: "STORAGE_STATS" }, undefined, false, false, guard), requested, true);
227
+ if (table.location !== undefined && table.location !== dataset.location)
228
+ fail("source_changed");
229
+ if (!same(await this.#attest(requested, signal), prepared))
230
+ fail("approval_changed");
231
+ await this.#transport.verify(prepared.principal, Math.min(deadline, this.#clock.now() + this.#bounds.httpMs), signal);
232
+ if (signal.aborted || this.#clock.now() >= deadline)
233
+ fail("local_stopped");
234
+ return cloneFrozen({
235
+ source: requested, consent: prepared, dataset, table, observedAt: timestamp(this.#clock.now()), responseBytes: bytes,
236
+ status: "inactive", queryAdmission: "blocked", costAdmission: "not-granted", projectPermissions: "unverified", billing: "unverified", sourceRights: "unreviewed", providerRetentionAuthorization: "not-granted",
237
+ });
238
+ }
239
+ catch (error) {
240
+ // The shared execution transport calls a missing resource result_expired;
241
+ // discovery never implies a result job existed.
242
+ if (error instanceof AnalyticalError && error.code === "result_expired")
243
+ fail("remote_failed");
244
+ throw error;
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,28 @@
1
+ import { type MetadataOptions, type MetadataSource, type MetadataLimits } from "./metadata-client.js";
2
+ import { type GoogleTokenResponse } from "./google-identity.js";
3
+ import { type Clock, type SafeFetch } from "./transport.js";
4
+ import { type PublicMetadataObservation } from "./public-metadata.js";
5
+ export interface MetadataFixtureHarnessConfig {
6
+ readonly sources: readonly MetadataSource[];
7
+ /** Explicit fixture identity and metadata transports; no live/default mode. */
8
+ readonly identityFetch: SafeFetch;
9
+ readonly metadataFetch: SafeFetch;
10
+ readonly clock?: Clock;
11
+ readonly limits?: Partial<MetadataLimits>;
12
+ }
13
+ /** Trusted application integration, never a command/JSON endpoint. Owner/session,
14
+ * source and project methods belong behind application-owner authentication.
15
+ * No snapshot setter, ledger, rows, jobs, storage or provider getter exists. */
16
+ export declare class MetadataFixtureHarness {
17
+ #private;
18
+ constructor(config: MetadataFixtureHarnessConfig);
19
+ setOwner(ownerId: string | undefined): void;
20
+ select(sourceId: string, selectedJobProject: string): void;
21
+ disconnect(): void;
22
+ denyMetadataConsent(): void;
23
+ connect(response: GoogleTokenResponse, signal?: AbortSignal): Promise<void>;
24
+ /** Explicit current owner gesture; token connection never grants this consent. */
25
+ consentToMetadata(): void;
26
+ discover(options?: MetadataOptions): Promise<PublicMetadataObservation>;
27
+ }
28
+ //# sourceMappingURL=metadata-harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-harness.d.ts","sourceRoot":"","sources":["../../src/analytical/metadata-harness.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6E,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACjL,OAAO,EAA2D,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAKzH,OAAO,EAAa,KAAK,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAE9F,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAC3C;AACD;;gFAEgF;AAChF,qBAAa,sBAAsB;;gBAYd,MAAM,EAAE,4BAA4B;IAwBhD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK3C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,IAAI;IAQ1D,UAAU,IAAI,IAAI;IAGlB,mBAAmB,IAAI,IAAI;IACrB,OAAO,CAAC,QAAQ,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF,kFAAkF;IAC3E,iBAAiB,IAAI,IAAI;IASnB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,yBAAyB,CAAC;CAwBzF"}
@@ -0,0 +1,127 @@
1
+ import { BigQueryMetadataClient } from "./metadata-client.js";
2
+ import { GoogleTokenIdentityProvider } from "./google-identity.js";
3
+ import { cloneFrozen, exactKeys, nonempty, project } from "./protocol.js";
4
+ import { same } from "./metadata.js";
5
+ import { opaqueID } from "./ledger.js";
6
+ import { fail } from "./wire.js";
7
+ import { realClock } from "./transport.js";
8
+ import { projectFixtureMetadata } from "./public-metadata.js";
9
+ /** Trusted application integration, never a command/JSON endpoint. Owner/session,
10
+ * source and project methods belong behind application-owner authentication.
11
+ * No snapshot setter, ledger, rows, jobs, storage or provider getter exists. */
12
+ export class MetadataFixtureHarness {
13
+ #provider;
14
+ #client;
15
+ #sources;
16
+ #clock;
17
+ #wallMs;
18
+ #owner;
19
+ #source;
20
+ #project;
21
+ #identity;
22
+ #binding;
23
+ #revision = 0;
24
+ constructor(config) {
25
+ exactKeys(config, ["sources", "identityFetch", "metadataFetch"], ["clock", "limits"]);
26
+ if (typeof config.identityFetch !== "function" || typeof config.metadataFetch !== "function")
27
+ fail("invalid_input");
28
+ this.#clock = config.clock ?? realClock;
29
+ this.#wallMs = config.limits?.wallMs ?? 30000;
30
+ this.#sources = new Map(cloneFrozen(config.sources).map(s => [s.sourceId, s]));
31
+ this.#provider = new GoogleTokenIdentityProvider({ fetch: config.identityFetch, now: () => this.#clock.now() });
32
+ this.#client = new BigQueryMetadataClient({ sources: config.sources, provider: this.#provider, fetch: config.metadataFetch, clock: this.#clock,
33
+ ...(config.limits === undefined ? {} : { limits: config.limits }),
34
+ authorizeMetadata: async (source) => {
35
+ const binding = this.#current(source);
36
+ if (!binding)
37
+ fail("approval_required");
38
+ return binding.consent;
39
+ }, currentMetadataBinding: source => this.#current(source),
40
+ });
41
+ }
42
+ #invalidate() { this.#revision += 1; this.#binding = undefined; }
43
+ #current(source) {
44
+ const b = this.#binding;
45
+ if (!b || !this.#identity || !this.#owner || !this.#source || !same(source, this.#source) ||
46
+ b.consent.ownerId !== this.#owner || b.consent.selectedJobProject !== this.#project ||
47
+ !same(b.consent.principal, this.#identity.principal))
48
+ return undefined;
49
+ return b;
50
+ }
51
+ setOwner(ownerId) {
52
+ this.disconnect();
53
+ this.#owner = undefined;
54
+ if (ownerId !== undefined) {
55
+ nonempty(ownerId);
56
+ this.#owner = ownerId;
57
+ }
58
+ }
59
+ select(sourceId, selectedJobProject) {
60
+ this.#invalidate();
61
+ this.#source = undefined;
62
+ this.#project = undefined;
63
+ const source = this.#sources.get(sourceId);
64
+ if (!source)
65
+ fail("policy_denied");
66
+ project(selectedJobProject);
67
+ this.#source = source;
68
+ this.#project = selectedJobProject;
69
+ }
70
+ disconnect() {
71
+ this.#invalidate();
72
+ this.#identity = undefined;
73
+ this.#provider.disconnect();
74
+ }
75
+ denyMetadataConsent() { this.#invalidate(); }
76
+ async connect(response, signal) {
77
+ this.disconnect();
78
+ const revision = this.#revision;
79
+ const identity = await this.#provider.connect(response, signal);
80
+ if (revision !== this.#revision) {
81
+ this.#provider.disconnect();
82
+ fail("approval_changed");
83
+ }
84
+ this.#identity = identity;
85
+ }
86
+ /** Explicit current owner gesture; token connection never grants this consent. */
87
+ consentToMetadata() {
88
+ this.#invalidate();
89
+ const i = this.#identity;
90
+ if (!this.#owner || !this.#source || !this.#project)
91
+ fail("approval_required");
92
+ if (!i)
93
+ fail("auth_required");
94
+ if (i.expiresAt <= this.#clock.now())
95
+ fail("auth_expired");
96
+ const consent = { purpose: "metadata-only", ownerId: this.#owner, consentId: opaqueID(), source: this.#source, principal: i.principal, selectedJobProject: this.#project };
97
+ this.#binding = cloneFrozen({ consent, read: i.read, expiresAt: i.expiresAt });
98
+ }
99
+ async discover(options = {}) {
100
+ exactKeys(options, [], ["signal", "deadline"]);
101
+ // Read caller accessors once, before asynchronous work. The client receives
102
+ // a plain snapshot; delivery never rereads mutable caller options.
103
+ const signal = options.signal;
104
+ const requestedDeadline = options.deadline;
105
+ const operationOptions = { ...(signal === undefined ? {} : { signal }), ...(requestedDeadline === undefined ? {} : { deadline: requestedDeadline }) };
106
+ const source = this.#source;
107
+ if (!source || !this.#current(source))
108
+ fail("approval_required");
109
+ const revision = this.#revision;
110
+ const deadline = Math.min(this.#clock.now() + this.#wallMs, requestedDeadline ?? Number.MAX_SAFE_INTEGER);
111
+ const discovery = await this.#client.discover(source.sourceId, operationOptions);
112
+ const result = await projectFixtureMetadata(discovery);
113
+ // Hashing is asynchronous. Read every externally supplied callback/accessor
114
+ // before the final protected guard, including an AbortSignal subclass getter.
115
+ const now = this.#clock.now();
116
+ const aborted = signal?.aborted;
117
+ const binding = this.#current(source);
118
+ if (binding && binding.expiresAt <= now)
119
+ fail("auth_expired");
120
+ if (aborted || now >= deadline)
121
+ fail("local_stopped");
122
+ // No consumer callback or caller property access may follow this guard.
123
+ if (revision !== this.#revision || !binding || !same(binding.consent, discovery.consent))
124
+ fail("approval_changed");
125
+ return result;
126
+ }
127
+ }
@@ -0,0 +1,8 @@
1
+ import { type JsonValue } from "./wire.js";
2
+ import { type Observation, type SchemaField, type SourceProfile } from "./protocol.js";
3
+ export declare function object(value: JsonValue | undefined): Record<string, JsonValue>;
4
+ export declare function decodeSchema(raw: JsonValue): readonly SchemaField[];
5
+ export declare function validateDataset(raw: JsonValue, source: SourceProfile): void;
6
+ export declare function same(a: unknown, b: unknown): boolean;
7
+ export declare function validateTable(raw: JsonValue, source: SourceProfile, now: number): Promise<Observation>;
8
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/analytical/metadata.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAA8G,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAOnM,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CACtC;AAIzC,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,WAAW,EAAE,CAoCnE;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAQ3E;AACD,wBAAgB,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAKpD;AACD,wBAAsB,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgF5G"}
@@ -0,0 +1,155 @@
1
+ import { canonicalJSON } from "./canonical.js";
2
+ import { fail, isObject } from "./wire.js";
3
+ import { cloneFrozen, digest, exactKeys, integerText, normalizeParameter, supportedField, timestamp, validateSchema } from "./protocol.js";
4
+ // Explicit classifications pinned to Google REST discovery / generated SDK v0.296.0.
5
+ const tableMetadata = "creationTime description etag expirationTime friendlyName id kind labels lastModifiedTime numActiveLogicalBytes numActivePhysicalBytes numBytes numCurrentPhysicalBytes numLongTermBytes numLongTermLogicalBytes numLongTermPhysicalBytes numPartitions numPhysicalBytes numRows numTimeTravelPhysicalBytes numTotalLogicalBytes numTotalPhysicalBytes resourceTags selfLink streamingBuffer".split(" ");
6
+ const tableRejected = "biglakeConfiguration cloneDefinition defaultCollation defaultRoundingMode encryptionConfiguration externalCatalogTableOptions externalDataConfiguration managedTableType materializedView materializedViewStatus maxStaleness model partitionDefinition replicas restrictions snapshotDefinition tableConstraints tableReplicationInfo view".split(" ");
7
+ const datasetMetadata = "access creationTime defaultPartitionExpirationMs defaultTableExpirationMs description etag friendlyName id kind labels lastModifiedTime maxTimeTravelHours resourceTags satisfiesPzi satisfiesPzs selfLink storageBillingModel tags".split(" ");
8
+ const datasetRejected = "catalogSource defaultCollation defaultEncryptionConfiguration defaultRoundingMode externalCatalogDatasetOptions externalDatasetReference isCaseInsensitive linkedDatasetMetadata linkedDatasetSource restrictions type".split(" ");
9
+ const fieldRejected = "collation dataGovernanceTagsInfo dataPolicies dataPolicyList defaultValueExpression foreignTypeDefinition generatedColumn maxLength policyTags rangeElementType roundingMode timestampPrecision".split(" ");
10
+ export function object(value) {
11
+ if (value === undefined || !isObject(value))
12
+ fail("malformed_wire");
13
+ return value;
14
+ }
15
+ function rejected(value, keys) {
16
+ for (const key of keys)
17
+ if (Object.hasOwn(value, key) && value[key] !== null)
18
+ fail("source_ineligible");
19
+ }
20
+ export function decodeSchema(raw) {
21
+ const schema = object(raw);
22
+ exactKeys(schema, ["fields"], [], "source_ineligible");
23
+ let count = 0;
24
+ const decode = (value, depth) => {
25
+ if (!Array.isArray(value) || value.length === 0 || depth > 8)
26
+ fail("malformed_wire");
27
+ return value.map(raw => {
28
+ if (++count > 1024)
29
+ fail("response_limit");
30
+ const field = object(raw);
31
+ exactKeys(field, ["name", "type"], ["mode", "fields", "precision", "scale", "description", "categories", ...fieldRejected], "source_ineligible");
32
+ rejected(field, fieldRejected);
33
+ if (typeof field.name !== "string" || typeof field.type !== "string" || field.name === "" || field.type === "")
34
+ fail("malformed_wire");
35
+ const mode = Object.hasOwn(field, "mode") ? field.mode : "NULLABLE";
36
+ if (typeof mode !== "string")
37
+ fail("malformed_wire");
38
+ const result = {
39
+ name: field.name, type: field.type, mode,
40
+ ...(Object.hasOwn(field, "fields") ? {
41
+ fields: decode(field.fields, depth + 1)
42
+ } : {}),
43
+ ...(Object.hasOwn(field, "precision") ? {
44
+ precision: integerText(field.precision)
45
+ } : {}),
46
+ ...(Object.hasOwn(field, "scale") ? {
47
+ scale: integerText(field.scale)
48
+ } : {}),
49
+ };
50
+ return result;
51
+ });
52
+ };
53
+ const result = decode(schema.fields, 0);
54
+ validateSchema(result);
55
+ return cloneFrozen(result);
56
+ }
57
+ export function validateDataset(raw, source) {
58
+ const value = object(raw);
59
+ exactKeys(value, ["datasetReference", "location"], [...datasetMetadata, ...datasetRejected], "source_ineligible");
60
+ rejected(value, datasetRejected);
61
+ const ref = object(value.datasetReference);
62
+ exactKeys(ref, ["projectId", "datasetId"], [], "source_ineligible");
63
+ if (ref.projectId !== source.sourceProject || ref.datasetId !== source.datasetId || value.location !== source.location)
64
+ fail("source_changed");
65
+ }
66
+ export function same(a, b) {
67
+ const encoder = new TextEncoder();
68
+ const aa = canonicalJSON(encoder.encode(JSON.stringify(a)));
69
+ const bb = canonicalJSON(encoder.encode(JSON.stringify(b)));
70
+ return aa.length === bb.length && aa.every((value, index) => value === bb[index]);
71
+ }
72
+ export async function validateTable(raw, source, now) {
73
+ const value = object(raw);
74
+ exactKeys(value, ["tableReference", "type", "schema"], ["location", "timePartitioning", "rangePartitioning", "clustering", "requirePartitionFilter", ...tableMetadata, ...tableRejected], "source_ineligible");
75
+ rejected(value, tableRejected);
76
+ const table = object(value.tableReference);
77
+ exactKeys(table, ["projectId", "datasetId", "tableId"], [], "source_ineligible");
78
+ if (table.projectId !== source.sourceProject || table.datasetId !== source.datasetId || table.tableId !== source.tableId || (Object.hasOwn(value, "location") && value.location !== source.location))
79
+ fail("source_changed");
80
+ if (value.type !== "TABLE")
81
+ fail("source_ineligible");
82
+ const schema = decodeSchema(value.schema);
83
+ if (!same(schema, source.schema))
84
+ fail("source_changed");
85
+ const fields = new Map(schema.map(field => [field.name, field]));
86
+ const config = Object.create(null);
87
+ for (const key of ["timePartitioning", "rangePartitioning", "clustering", "requirePartitionFilter"]) {
88
+ if (!Object.hasOwn(value, key))
89
+ continue;
90
+ const item = value[key];
91
+ if (item === null)
92
+ fail("source_ineligible");
93
+ if (key === "requirePartitionFilter") {
94
+ if (typeof item !== "boolean")
95
+ fail("source_ineligible");
96
+ }
97
+ else {
98
+ const settings = object(item);
99
+ if (key === "clustering") {
100
+ exactKeys(settings, ["fields"], [], "source_ineligible");
101
+ if (!Array.isArray(settings.fields) || settings.fields.length < 1 || settings.fields.length > 4 || new Set(settings.fields).size !== settings.fields.length)
102
+ fail("source_ineligible");
103
+ for (const name of settings.fields) {
104
+ const field = typeof name === "string" ? fields.get(name) : undefined;
105
+ if (field === undefined || field.mode === "REPEATED" || field.fields !== undefined || ["BOOL", "BOOLEAN", "BYTES", "JSON", "RECORD", "STRUCT", "GEOGRAPHY", "RANGE", "INTERVAL"].includes(field.type))
106
+ fail("source_ineligible");
107
+ supportedField(field);
108
+ }
109
+ }
110
+ else if (key === "timePartitioning") {
111
+ if (Object.hasOwn(value, "rangePartitioning"))
112
+ fail("source_ineligible");
113
+ exactKeys(settings, ["type"], ["field", "expirationMs", "requirePartitionFilter"], "source_ineligible");
114
+ if (!["DAY", "HOUR", "MONTH", "YEAR"].includes(String(settings.type)))
115
+ fail("source_ineligible");
116
+ if (Object.hasOwn(settings, "field")) {
117
+ const field = typeof settings.field === "string" ? fields.get(settings.field) : undefined;
118
+ if (field === undefined || !["DATE", "TIMESTAMP", "DATETIME"].includes(field.type))
119
+ fail("source_ineligible");
120
+ }
121
+ if (Object.hasOwn(settings, "expirationMs"))
122
+ integerText(settings.expirationMs);
123
+ if (Object.hasOwn(settings, "requirePartitionFilter") && typeof settings.requirePartitionFilter !== "boolean")
124
+ fail("source_ineligible");
125
+ if (Object.hasOwn(settings, "requirePartitionFilter") && Object.hasOwn(value, "requirePartitionFilter") && settings.requirePartitionFilter !== value.requirePartitionFilter)
126
+ fail("source_ineligible");
127
+ }
128
+ else {
129
+ exactKeys(settings, ["field", "range"], [], "source_ineligible");
130
+ const field = typeof settings.field === "string" ? fields.get(settings.field) : undefined;
131
+ if (field === undefined || !["INTEGER", "INT64"].includes(field.type))
132
+ fail("source_ineligible");
133
+ const range = object(settings.range);
134
+ exactKeys(range, ["start", "end", "interval"], [], "source_ineligible");
135
+ const values = ["start", "end", "interval"].map(key => {
136
+ const cell = normalizeParameter({
137
+ name: "", type: "INT64"
138
+ }, range[key]);
139
+ return BigInt(cell);
140
+ });
141
+ if (values[0] >= values[1] || values[2] < 1n)
142
+ fail("source_ineligible");
143
+ }
144
+ }
145
+ config[key] = item;
146
+ }
147
+ const payload = {
148
+ table: {
149
+ projectId: source.sourceProject, datasetId: source.datasetId, tableId: source.tableId
150
+ }, location: source.location, type: "TABLE", config, schema
151
+ };
152
+ return cloneFrozen({
153
+ ...payload, digest: await digest("Observation", payload), observedAt: timestamp(now)
154
+ });
155
+ }