@flytedesk/app-kit 0.3.1 → 0.4.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 +6 -3
- package/dist/bigquery/client.d.ts +18 -0
- package/dist/bigquery/client.js +37 -0
- package/dist/bigquery/client.js.map +1 -0
- package/dist/bigquery/errors.d.ts +26 -0
- package/dist/bigquery/errors.js +80 -0
- package/dist/bigquery/errors.js.map +1 -0
- package/dist/bigquery/extract.d.ts +15 -0
- package/dist/bigquery/extract.js +88 -0
- package/dist/bigquery/extract.js.map +1 -0
- package/dist/bigquery/index.d.ts +47 -0
- package/dist/bigquery/index.js +46 -0
- package/dist/bigquery/index.js.map +1 -0
- package/dist/bigquery/labels.d.ts +16 -0
- package/dist/bigquery/labels.js +26 -0
- package/dist/bigquery/labels.js.map +1 -0
- package/dist/bigquery/query.d.ts +16 -0
- package/dist/bigquery/query.js +86 -0
- package/dist/bigquery/query.js.map +1 -0
- package/dist/bigquery/types.d.ts +155 -0
- package/dist/bigquery/types.js +15 -0
- package/dist/bigquery/types.js.map +1 -0
- package/dist/chat/callback-secret.d.ts +21 -0
- package/dist/chat/callback-secret.js +33 -0
- package/dist/chat/callback-secret.js.map +1 -0
- package/dist/chat/env.d.ts +27 -0
- package/dist/chat/env.js +44 -0
- package/dist/chat/env.js.map +1 -0
- package/dist/chat/index.d.ts +72 -0
- package/dist/chat/index.js +70 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/launcher.d.ts +43 -0
- package/dist/chat/launcher.js +103 -0
- package/dist/chat/launcher.js.map +1 -0
- package/dist/chat/mcp-protocol.d.ts +96 -0
- package/dist/chat/mcp-protocol.js +200 -0
- package/dist/chat/mcp-protocol.js.map +1 -0
- package/dist/chat/mcp-server.d.ts +17 -0
- package/dist/chat/mcp-server.js +71 -0
- package/dist/chat/mcp-server.js.map +1 -0
- package/dist/chat/types.d.ts +91 -0
- package/dist/chat/types.js +5 -0
- package/dist/chat/types.js.map +1 -0
- package/package.json +10 -1
- package/scripts/pending-release-count.mjs +37 -0
- package/scripts/release.sh +126 -0
- package/scripts/release.test.ts +205 -0
package/README.md
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
Shared platform package for every flytedesk app — the Node/TypeScript equivalent of
|
|
4
4
|
what `danx` is for the PHP apps.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Five subpath modules, one npm package:
|
|
7
7
|
|
|
8
8
|
- `@flytedesk/app-kit/auth` — the flytedesk-id BFF/OIDC client (PKCE, session issuance,
|
|
9
9
|
live per-request authorization).
|
|
10
10
|
- `@flytedesk/app-kit/trace` — a Postgres-native trace/audit layer (HTTP request → job →
|
|
11
11
|
DB write → logs, one queryable trace), buffered through Pub/Sub, with a scheduled
|
|
12
12
|
retention/prune job.
|
|
13
|
+
- `@flytedesk/app-kit/rate-limit` — request rate-limiting and quota enforcement.
|
|
14
|
+
- `@flytedesk/app-kit/profile` — user profile and preferences management.
|
|
15
|
+
- `@flytedesk/app-kit/flags` — feature flags and A/B testing.
|
|
13
16
|
|
|
14
17
|
Status: **bootstrapping.** Both modules are placeholders. See media-planner's published
|
|
15
18
|
"Media Planner Backend" architecture doc for the full design (DEC-25 through DEC-34) and
|
|
@@ -25,5 +28,5 @@ pnpm add @flytedesk/app-kit
|
|
|
25
28
|
## Publishing
|
|
26
29
|
|
|
27
30
|
Published to the public npm registry under the `@flytedesk` org, access `restricted`.
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
The CI publish workflow (`.github/workflows/release.yml`) automatically publishes on pushes
|
|
32
|
+
to main using OIDC Trusted Publishing — no manual token or NPM_TOKEN secret required.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BigQueryLike, DryRunEstimate, EstimateQueryBytesOptions, ExtractJobHandle, ExtractJobStatus, ExtractTableToGCSInput, JobLabelInput, QueryPage, RunQueryOptions, WaitForExtractJobOptions } from "./types.js";
|
|
2
|
+
export interface CreateBigQueryReadClientOptions {
|
|
3
|
+
/** Merged under any per-call `labels`/`extra` — e.g. `{app: "sms-app"}`. */
|
|
4
|
+
defaultLabels?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface BigQueryReadClient {
|
|
7
|
+
query<T = Record<string, unknown>>(options: RunQueryOptions): Promise<QueryPage<T>>;
|
|
8
|
+
queryAllPages<T = Record<string, unknown>>(options: RunQueryOptions): AsyncGenerator<T[], void, void>;
|
|
9
|
+
estimateQueryBytes(options: EstimateQueryBytesOptions): Promise<DryRunEstimate>;
|
|
10
|
+
extractTableToGCS(input: ExtractTableToGCSInput): Promise<ExtractJobHandle>;
|
|
11
|
+
getExtractJobStatus(jobId: string): Promise<ExtractJobStatus>;
|
|
12
|
+
waitForExtractJob(jobId: string, options?: WaitForExtractJobOptions): Promise<ExtractJobStatus>;
|
|
13
|
+
/** Builds a `{actor, purpose, ...extra}` label set, pre-merged with this
|
|
14
|
+
* client's `defaultLabels` — pass the result as `labels` to `query`/
|
|
15
|
+
* `extractTableToGCS`. */
|
|
16
|
+
buildJobLabels(input: JobLabelInput): Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createBigQueryReadClient(bq: BigQueryLike, options?: CreateBigQueryReadClientOptions): BigQueryReadClient;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createBigQueryReadClient(bq) — bundles query/dry-run/extract into one
|
|
3
|
+
* object bound to the caller's BigQueryLike (a real `@google-cloud/bigquery`
|
|
4
|
+
* `BigQuery` instance satisfies this structurally — see types.ts), same
|
|
5
|
+
* binder shape as src/trace/client.ts's createTraceClient. `defaultLabels`
|
|
6
|
+
* (e.g. `{app: "sms-app"}`) are merged under any per-call `labels`, so a
|
|
7
|
+
* caller only needs to supply what's specific to that call.
|
|
8
|
+
*/
|
|
9
|
+
import { buildJobLabels } from "./labels.js";
|
|
10
|
+
import { extractTableToGCS, getExtractJobStatus, waitForExtractJob, } from "./extract.js";
|
|
11
|
+
import { estimateQueryBytes, runQuery, runQueryAllPages } from "./query.js";
|
|
12
|
+
export function createBigQueryReadClient(bq, options = {}) {
|
|
13
|
+
const defaultLabels = options.defaultLabels ?? {};
|
|
14
|
+
const withDefaults = (labels) => ({
|
|
15
|
+
...defaultLabels,
|
|
16
|
+
...labels,
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
query: (queryOptions) => runQuery(bq, {
|
|
20
|
+
...queryOptions,
|
|
21
|
+
labels: withDefaults(queryOptions.labels),
|
|
22
|
+
}),
|
|
23
|
+
queryAllPages: (queryOptions) => runQueryAllPages(bq, {
|
|
24
|
+
...queryOptions,
|
|
25
|
+
labels: withDefaults(queryOptions.labels),
|
|
26
|
+
}),
|
|
27
|
+
estimateQueryBytes: (estimateOptions) => estimateQueryBytes(bq, estimateOptions),
|
|
28
|
+
extractTableToGCS: (input) => extractTableToGCS(bq, { ...input, labels: withDefaults(input.labels) }),
|
|
29
|
+
getExtractJobStatus: (jobId) => getExtractJobStatus(bq, jobId),
|
|
30
|
+
waitForExtractJob: (jobId, waitOptions) => waitForExtractJob(bq, jobId, waitOptions),
|
|
31
|
+
buildJobLabels: (input) => ({
|
|
32
|
+
...defaultLabels,
|
|
33
|
+
...buildJobLabels(input),
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/bigquery/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAyC5E,MAAM,UAAU,wBAAwB,CACtC,EAAgB,EAChB,UAA2C,EAAE;IAE7C,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;IAClD,MAAM,YAAY,GAAG,CAAC,MAA+B,EAAE,EAAE,CAAC,CAAC;QACzD,GAAG,aAAa;QAChB,GAAG,MAAM;KACV,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,CAAC,YAAY,EAAE,EAAE,CACtB,QAAQ,CAAC,EAAE,EAAE;YACX,GAAG,YAAY;YACf,MAAM,EAAE,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC;SAC1C,CAAC;QACJ,aAAa,EAAE,CAAC,YAAY,EAAE,EAAE,CAC9B,gBAAgB,CAAC,EAAE,EAAE;YACnB,GAAG,YAAY;YACf,MAAM,EAAE,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC;SAC1C,CAAC;QACJ,kBAAkB,EAAE,CAAC,eAAe,EAAE,EAAE,CACtC,kBAAkB,CAAC,EAAE,EAAE,eAAe,CAAC;QACzC,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3B,iBAAiB,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACzE,mBAAmB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,mBAAmB,CAAC,EAAE,EAAE,KAAK,CAAC;QAC9D,iBAAiB,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CACxC,iBAAiB,CAAC,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC;QAC3C,cAAc,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1B,GAAG,aAAa;YAChB,GAAG,cAAc,CAAC,KAAK,CAAC;SACzB,CAAC;KACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class BigQueryReadError extends Error {
|
|
2
|
+
readonly cause?: unknown | undefined;
|
|
3
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
4
|
+
}
|
|
5
|
+
/** Malformed / unparseable SQL, or a bad parameter binding. */
|
|
6
|
+
export declare class BadQueryError extends BigQueryReadError {
|
|
7
|
+
constructor(message: string, cause?: unknown);
|
|
8
|
+
}
|
|
9
|
+
/** The caller's credentials don't have access to the dataset/table/job. */
|
|
10
|
+
export declare class PermissionDeniedError extends BigQueryReadError {
|
|
11
|
+
constructor(message: string, cause?: unknown);
|
|
12
|
+
}
|
|
13
|
+
/** A dry-run estimate exceeded the caller-supplied byte budget — thrown
|
|
14
|
+
* before any billable query actually runs. */
|
|
15
|
+
export declare class DryRunBudgetExceededError extends BigQueryReadError {
|
|
16
|
+
readonly totalBytesProcessed: number;
|
|
17
|
+
readonly maxBytesBilled: number;
|
|
18
|
+
constructor(totalBytesProcessed: number, maxBytesBilled: number);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes anything a BigQueryLike call can throw/reject with (a Google API
|
|
22
|
+
* client error, a job's `status.errorResult`/`status.errors`, or an already-
|
|
23
|
+
* classified BigQueryReadError) into a BigQueryReadError subclass. Never
|
|
24
|
+
* throws itself — always returns an Error to `throw` at the call site.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyBigQueryError(err: unknown): BigQueryReadError;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export class BigQueryReadError extends Error {
|
|
2
|
+
cause;
|
|
3
|
+
constructor(message, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.cause = cause;
|
|
6
|
+
this.name = "BigQueryReadError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Malformed / unparseable SQL, or a bad parameter binding. */
|
|
10
|
+
export class BadQueryError extends BigQueryReadError {
|
|
11
|
+
constructor(message, cause) {
|
|
12
|
+
super(message, cause);
|
|
13
|
+
this.name = "BadQueryError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The caller's credentials don't have access to the dataset/table/job. */
|
|
17
|
+
export class PermissionDeniedError extends BigQueryReadError {
|
|
18
|
+
constructor(message, cause) {
|
|
19
|
+
super(message, cause);
|
|
20
|
+
this.name = "PermissionDeniedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** A dry-run estimate exceeded the caller-supplied byte budget — thrown
|
|
24
|
+
* before any billable query actually runs. */
|
|
25
|
+
export class DryRunBudgetExceededError extends BigQueryReadError {
|
|
26
|
+
totalBytesProcessed;
|
|
27
|
+
maxBytesBilled;
|
|
28
|
+
constructor(totalBytesProcessed, maxBytesBilled) {
|
|
29
|
+
super(`Dry-run estimate of ${totalBytesProcessed} bytes exceeds the ${maxBytesBilled} byte budget`);
|
|
30
|
+
this.totalBytesProcessed = totalBytesProcessed;
|
|
31
|
+
this.maxBytesBilled = maxBytesBilled;
|
|
32
|
+
this.name = "DryRunBudgetExceededError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Reason codes the Google APIs client library attaches to a rejected job
|
|
36
|
+
* promise's `.errors[]` — see
|
|
37
|
+
* https://cloud.google.com/bigquery/docs/error-messages for the full list.
|
|
38
|
+
* Anything not in these two sets falls through to the generic
|
|
39
|
+
* BigQueryReadError rather than being mis-classified. */
|
|
40
|
+
const PERMISSION_DENIED_REASONS = new Set([
|
|
41
|
+
"accessDenied",
|
|
42
|
+
"forbidden",
|
|
43
|
+
"insufficientPermissions",
|
|
44
|
+
]);
|
|
45
|
+
const BAD_QUERY_REASONS = new Set([
|
|
46
|
+
"invalidQuery",
|
|
47
|
+
"invalid",
|
|
48
|
+
"parseError",
|
|
49
|
+
"invalidQueryParameter",
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes anything a BigQueryLike call can throw/reject with (a Google API
|
|
53
|
+
* client error, a job's `status.errorResult`/`status.errors`, or an already-
|
|
54
|
+
* classified BigQueryReadError) into a BigQueryReadError subclass. Never
|
|
55
|
+
* throws itself — always returns an Error to `throw` at the call site.
|
|
56
|
+
*/
|
|
57
|
+
export function classifyBigQueryError(err) {
|
|
58
|
+
if (err instanceof BigQueryReadError)
|
|
59
|
+
return err;
|
|
60
|
+
const apiErr = err;
|
|
61
|
+
const detail = apiErr?.errors?.[0];
|
|
62
|
+
const reason = detail?.reason;
|
|
63
|
+
const message = detail?.message ??
|
|
64
|
+
apiErr?.message ??
|
|
65
|
+
(err instanceof Error ? err.message : String(err));
|
|
66
|
+
if (reason && PERMISSION_DENIED_REASONS.has(reason)) {
|
|
67
|
+
return new PermissionDeniedError(message, err);
|
|
68
|
+
}
|
|
69
|
+
if (apiErr?.code === 403) {
|
|
70
|
+
return new PermissionDeniedError(message, err);
|
|
71
|
+
}
|
|
72
|
+
if (reason && BAD_QUERY_REASONS.has(reason)) {
|
|
73
|
+
return new BadQueryError(message, err);
|
|
74
|
+
}
|
|
75
|
+
if (apiErr?.code === 400) {
|
|
76
|
+
return new BadQueryError(message, err);
|
|
77
|
+
}
|
|
78
|
+
return new BigQueryReadError(message, err);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/bigquery/errors.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAGxB;IAFlB,YACE,OAAe,EACC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,UAAK,GAAL,KAAK,CAAU;QAG/B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,+DAA+D;AAC/D,MAAM,OAAO,aAAc,SAAQ,iBAAiB;IAClD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,OAAO,qBAAsB,SAAQ,iBAAiB;IAC1D,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;+CAC+C;AAC/C,MAAM,OAAO,yBAA0B,SAAQ,iBAAiB;IAE5C;IACA;IAFlB,YACkB,mBAA2B,EAC3B,cAAsB;QAEtC,KAAK,CACH,uBAAuB,mBAAmB,sBAAsB,cAAc,cAAc,CAC7F,CAAC;QALc,wBAAmB,GAAnB,mBAAmB,CAAQ;QAC3B,mBAAc,GAAd,cAAc,CAAQ;QAKtC,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAQD;;;;0DAI0D;AAC1D,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IACxC,cAAc;IACd,WAAW;IACX,yBAAyB;CAC1B,CAAC,CAAC;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,cAAc;IACd,SAAS;IACT,YAAY;IACZ,uBAAuB;CACxB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,GAAG,YAAY,iBAAiB;QAAE,OAAO,GAAG,CAAC;IAEjD,MAAM,MAAM,GAAG,GAAyB,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC;IAC9B,MAAM,OAAO,GACX,MAAM,EAAE,OAAO;QACf,MAAM,EAAE,OAAO;QACf,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAErD,IAAI,MAAM,IAAI,yBAAyB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACpD,OAAO,IAAI,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,EAAE,IAAI,KAAK,GAAG,EAAE,CAAC;QACzB,OAAO,IAAI,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,IAAI,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,MAAM,EAAE,IAAI,KAAK,GAAG,EAAE,CAAC;QACzB,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BigQueryLike, ExtractJobHandle, ExtractJobStatus, ExtractTableToGCSInput, WaitForExtractJobOptions } from "./types.js";
|
|
2
|
+
/** Submits a table extract job and returns immediately with its job id — does
|
|
3
|
+
* NOT wait for completion. Use `getExtractJobStatus`/`waitForExtractJob` to
|
|
4
|
+
* observe it. */
|
|
5
|
+
export declare function extractTableToGCS(bq: BigQueryLike, input: ExtractTableToGCSInput): Promise<ExtractJobHandle>;
|
|
6
|
+
/** Reads the current status of a previously-submitted extract job by id —
|
|
7
|
+
* a single, non-blocking status check. */
|
|
8
|
+
export declare function getExtractJobStatus(bq: BigQueryLike, jobId: string): Promise<ExtractJobStatus>;
|
|
9
|
+
/**
|
|
10
|
+
* Polls an extract job until it reaches `DONE`, or throws once
|
|
11
|
+
* `timeoutMs` elapses without that happening. Throws a classified
|
|
12
|
+
* BigQueryReadError if the job finishes with `status.errorResult`/`errors`
|
|
13
|
+
* set (e.g. permission denied on the destination bucket).
|
|
14
|
+
*/
|
|
15
|
+
export declare function waitForExtractJob(bq: BigQueryLike, jobId: string, options?: WaitForExtractJobOptions): Promise<ExtractJobStatus>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract-to-Cloud-Storage for large table exports, with job-status polling —
|
|
3
|
+
* the other generic capability out of media-planner's inventoryWarehouse.ts
|
|
4
|
+
* (AK-11).
|
|
5
|
+
*/
|
|
6
|
+
import { BigQueryReadError, classifyBigQueryError } from "./errors.js";
|
|
7
|
+
const DEFAULT_FORMAT = "NEWLINE_DELIMITED_JSON";
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
10
|
+
const KNOWN_STATES = new Set([
|
|
11
|
+
"PENDING",
|
|
12
|
+
"RUNNING",
|
|
13
|
+
"DONE",
|
|
14
|
+
]);
|
|
15
|
+
function toExtractJobStatus(metadata) {
|
|
16
|
+
const rawState = metadata.status?.state;
|
|
17
|
+
const state = KNOWN_STATES.has(rawState ?? "")
|
|
18
|
+
? rawState
|
|
19
|
+
: "UNKNOWN";
|
|
20
|
+
const errors = metadata.status?.errors ??
|
|
21
|
+
(metadata.status?.errorResult ? [metadata.status.errorResult] : undefined);
|
|
22
|
+
return {
|
|
23
|
+
jobId: metadata.id ?? "",
|
|
24
|
+
state,
|
|
25
|
+
done: state === "DONE",
|
|
26
|
+
errors,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Submits a table extract job and returns immediately with its job id — does
|
|
30
|
+
* NOT wait for completion. Use `getExtractJobStatus`/`waitForExtractJob` to
|
|
31
|
+
* observe it. */
|
|
32
|
+
export async function extractTableToGCS(bq, input) {
|
|
33
|
+
try {
|
|
34
|
+
const table = bq.dataset(input.datasetId).table(input.tableId);
|
|
35
|
+
const [job] = await table.createExtractJob(input.destination, {
|
|
36
|
+
format: input.format ?? DEFAULT_FORMAT,
|
|
37
|
+
gzip: input.gzip,
|
|
38
|
+
labels: input.labels,
|
|
39
|
+
});
|
|
40
|
+
return { jobId: job.id };
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw classifyBigQueryError(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Reads the current status of a previously-submitted extract job by id —
|
|
47
|
+
* a single, non-blocking status check. */
|
|
48
|
+
export async function getExtractJobStatus(bq, jobId) {
|
|
49
|
+
try {
|
|
50
|
+
const job = bq.job(jobId);
|
|
51
|
+
const [metadata] = await job.getMetadata();
|
|
52
|
+
return toExtractJobStatus({ ...metadata, id: metadata.id ?? jobId });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw classifyBigQueryError(err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function sleep(ms) {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Polls an extract job until it reaches `DONE`, or throws once
|
|
63
|
+
* `timeoutMs` elapses without that happening. Throws a classified
|
|
64
|
+
* BigQueryReadError if the job finishes with `status.errorResult`/`errors`
|
|
65
|
+
* set (e.g. permission denied on the destination bucket).
|
|
66
|
+
*/
|
|
67
|
+
export async function waitForExtractJob(bq, jobId, options = {}) {
|
|
68
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
69
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
70
|
+
const deadline = Date.now() + timeoutMs;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const status = await getExtractJobStatus(bq, jobId);
|
|
73
|
+
if (status.done) {
|
|
74
|
+
if (status.errors && status.errors.length > 0) {
|
|
75
|
+
throw classifyBigQueryError({
|
|
76
|
+
errors: status.errors,
|
|
77
|
+
message: status.errors[0]?.message,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return status;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() >= deadline) {
|
|
83
|
+
throw new BigQueryReadError(`Extract job ${jobId} did not complete within ${timeoutMs}ms (last state: ${status.state})`);
|
|
84
|
+
}
|
|
85
|
+
await sleep(pollIntervalMs);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/bigquery/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only BigQuery client (AK-11): parameterised queries, dry-run byte
|
|
3
|
+
* estimation, job labels, paged results, and extract-to-Cloud-Storage with
|
|
4
|
+
* job-status polling — generalized out of media-planner's
|
|
5
|
+
* `apps/api/src/services/inventoryWarehouse.ts`. Inventory-specific SQL stays
|
|
6
|
+
* in media-planner; this module only ever runs SQL/params the caller supplies.
|
|
7
|
+
*
|
|
8
|
+
* No hard dependency on `@google-cloud/bigquery` — every function takes a
|
|
9
|
+
* structural `BigQueryLike` (see types.ts), which a real `new BigQuery()`
|
|
10
|
+
* instance already satisfies:
|
|
11
|
+
*
|
|
12
|
+
* import { BigQuery } from "@google-cloud/bigquery";
|
|
13
|
+
* import { createBigQueryReadClient } from "@flytedesk/app-kit/bigquery";
|
|
14
|
+
*
|
|
15
|
+
* const client = createBigQueryReadClient(new BigQuery(), {
|
|
16
|
+
* defaultLabels: { app: "sms-app" },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const estimate = await client.estimateQueryBytes({
|
|
20
|
+
* sql: "SELECT * FROM `project.dataset.contacts` WHERE campaign_id = @campaignId",
|
|
21
|
+
* params: { campaignId },
|
|
22
|
+
* maxBytesBilled: 10 * 1024 ** 3, // throws DryRunBudgetExceededError past 10 GiB
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const page = await client.query({
|
|
26
|
+
* sql: "SELECT * FROM `project.dataset.contacts` WHERE campaign_id = @campaignId",
|
|
27
|
+
* params: { campaignId },
|
|
28
|
+
* labels: client.buildJobLabels({ actor: userId, purpose: "audience-export" }),
|
|
29
|
+
* pageSize: 500,
|
|
30
|
+
* });
|
|
31
|
+
* // page.nextPageToken, if present, resumes with { ...same options, pageToken }
|
|
32
|
+
*
|
|
33
|
+
* const { jobId } = await client.extractTableToGCS({
|
|
34
|
+
* datasetId: "dataset",
|
|
35
|
+
* tableId: "campaign_results",
|
|
36
|
+
* destination: storage.bucket("exports").file("campaign_results.json.gz"),
|
|
37
|
+
* gzip: true,
|
|
38
|
+
* });
|
|
39
|
+
* const status = await client.waitForExtractJob(jobId);
|
|
40
|
+
*/
|
|
41
|
+
export { createBigQueryReadClient } from "./client.js";
|
|
42
|
+
export type { BigQueryReadClient, CreateBigQueryReadClientOptions, } from "./client.js";
|
|
43
|
+
export { runQuery, runQueryAllPages, estimateQueryBytes } from "./query.js";
|
|
44
|
+
export { extractTableToGCS, getExtractJobStatus, waitForExtractJob, } from "./extract.js";
|
|
45
|
+
export { buildJobLabels, sanitizeLabelValue } from "./labels.js";
|
|
46
|
+
export { BadQueryError, BigQueryReadError, DryRunBudgetExceededError, PermissionDeniedError, classifyBigQueryError, } from "./errors.js";
|
|
47
|
+
export type { BigQueryApiErrorDetail, BigQueryCreateQueryJobOptions, BigQueryDatasetLike, BigQueryExtractOptions, BigQueryJobLike, BigQueryJobMetadata, BigQueryLike, BigQueryNextQuery, BigQueryQueryResultsOptions, BigQueryTableLike, DryRunEstimate, EstimateQueryBytesOptions, ExtractJobHandle, ExtractJobState, ExtractJobStatus, ExtractTableToGCSInput, GcsFileLike, JobLabelInput, QueryPage, RunQueryOptions, WaitForExtractJobOptions, } from "./types.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only BigQuery client (AK-11): parameterised queries, dry-run byte
|
|
3
|
+
* estimation, job labels, paged results, and extract-to-Cloud-Storage with
|
|
4
|
+
* job-status polling — generalized out of media-planner's
|
|
5
|
+
* `apps/api/src/services/inventoryWarehouse.ts`. Inventory-specific SQL stays
|
|
6
|
+
* in media-planner; this module only ever runs SQL/params the caller supplies.
|
|
7
|
+
*
|
|
8
|
+
* No hard dependency on `@google-cloud/bigquery` — every function takes a
|
|
9
|
+
* structural `BigQueryLike` (see types.ts), which a real `new BigQuery()`
|
|
10
|
+
* instance already satisfies:
|
|
11
|
+
*
|
|
12
|
+
* import { BigQuery } from "@google-cloud/bigquery";
|
|
13
|
+
* import { createBigQueryReadClient } from "@flytedesk/app-kit/bigquery";
|
|
14
|
+
*
|
|
15
|
+
* const client = createBigQueryReadClient(new BigQuery(), {
|
|
16
|
+
* defaultLabels: { app: "sms-app" },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const estimate = await client.estimateQueryBytes({
|
|
20
|
+
* sql: "SELECT * FROM `project.dataset.contacts` WHERE campaign_id = @campaignId",
|
|
21
|
+
* params: { campaignId },
|
|
22
|
+
* maxBytesBilled: 10 * 1024 ** 3, // throws DryRunBudgetExceededError past 10 GiB
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const page = await client.query({
|
|
26
|
+
* sql: "SELECT * FROM `project.dataset.contacts` WHERE campaign_id = @campaignId",
|
|
27
|
+
* params: { campaignId },
|
|
28
|
+
* labels: client.buildJobLabels({ actor: userId, purpose: "audience-export" }),
|
|
29
|
+
* pageSize: 500,
|
|
30
|
+
* });
|
|
31
|
+
* // page.nextPageToken, if present, resumes with { ...same options, pageToken }
|
|
32
|
+
*
|
|
33
|
+
* const { jobId } = await client.extractTableToGCS({
|
|
34
|
+
* datasetId: "dataset",
|
|
35
|
+
* tableId: "campaign_results",
|
|
36
|
+
* destination: storage.bucket("exports").file("campaign_results.json.gz"),
|
|
37
|
+
* gzip: true,
|
|
38
|
+
* });
|
|
39
|
+
* const status = await client.waitForExtractJob(jobId);
|
|
40
|
+
*/
|
|
41
|
+
export { createBigQueryReadClient } from "./client.js";
|
|
42
|
+
export { runQuery, runQueryAllPages, estimateQueryBytes } from "./query.js";
|
|
43
|
+
export { extractTableToGCS, getExtractJobStatus, waitForExtractJob, } from "./extract.js";
|
|
44
|
+
export { buildJobLabels, sanitizeLabelValue } from "./labels.js";
|
|
45
|
+
export { BadQueryError, BigQueryReadError, DryRunBudgetExceededError, PermissionDeniedError, classifyBigQueryError, } from "./errors.js";
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bigquery/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAMvD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job-label helpers — BigQuery job labels are billing/audit metadata attached
|
|
3
|
+
* to every query/extract job ("who ran it, for what"), constrained to keys and
|
|
4
|
+
* values matching `[a-z0-9_-]{1,63}` (lowercase only, no spaces). Ported from
|
|
5
|
+
* media-planner's inventoryWarehouse job-labeling convention, generalized to
|
|
6
|
+
* take an arbitrary actor/purpose pair rather than inventory-specific fields.
|
|
7
|
+
*/
|
|
8
|
+
import type { JobLabelInput } from "./types.js";
|
|
9
|
+
/** Coerces an arbitrary string into a valid BigQuery label value: lowercased,
|
|
10
|
+
* invalid characters replaced with `-`, truncated to 63 characters. Falls
|
|
11
|
+
* back to `"unknown"` for an empty/all-invalid input, since an empty label
|
|
12
|
+
* value is itself rejected by BigQuery. */
|
|
13
|
+
export declare function sanitizeLabelValue(value: string): string;
|
|
14
|
+
/** Builds a `{actor, purpose, ...extra}` label set for a job, sanitizing
|
|
15
|
+
* every key and value to BigQuery's label format. */
|
|
16
|
+
export declare function buildJobLabels(input: JobLabelInput): Record<string, string>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const MAX_LABEL_LENGTH = 63;
|
|
2
|
+
const INVALID_LABEL_CHARS = /[^a-z0-9_-]/g;
|
|
3
|
+
/** Coerces an arbitrary string into a valid BigQuery label value: lowercased,
|
|
4
|
+
* invalid characters replaced with `-`, truncated to 63 characters. Falls
|
|
5
|
+
* back to `"unknown"` for an empty/all-invalid input, since an empty label
|
|
6
|
+
* value is itself rejected by BigQuery. */
|
|
7
|
+
export function sanitizeLabelValue(value) {
|
|
8
|
+
const sanitized = value
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(INVALID_LABEL_CHARS, "-")
|
|
11
|
+
.slice(0, MAX_LABEL_LENGTH);
|
|
12
|
+
return sanitized.length > 0 ? sanitized : "unknown";
|
|
13
|
+
}
|
|
14
|
+
/** Builds a `{actor, purpose, ...extra}` label set for a job, sanitizing
|
|
15
|
+
* every key and value to BigQuery's label format. */
|
|
16
|
+
export function buildJobLabels(input) {
|
|
17
|
+
const labels = {
|
|
18
|
+
actor: sanitizeLabelValue(input.actor),
|
|
19
|
+
purpose: sanitizeLabelValue(input.purpose),
|
|
20
|
+
};
|
|
21
|
+
for (const [key, value] of Object.entries(input.extra ?? {})) {
|
|
22
|
+
labels[sanitizeLabelValue(key)] = sanitizeLabelValue(value);
|
|
23
|
+
}
|
|
24
|
+
return labels;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../../src/bigquery/labels.ts"],"names":[],"mappings":"AASA,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAE3C;;;4CAG4C;AAC5C,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,SAAS,GAAG,KAAK;SACpB,WAAW,EAAE;SACb,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC;SACjC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAC9B,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC;AAED;sDACsD;AACtD,MAAM,UAAU,cAAc,CAAC,KAAoB;IACjD,MAAM,MAAM,GAA2B;QACrC,KAAK,EAAE,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC;QACtC,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC;KAC3C,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;QAC7D,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BigQueryLike, DryRunEstimate, EstimateQueryBytesOptions, QueryPage, RunQueryOptions } from "./types.js";
|
|
2
|
+
/** Runs one page of a parameterised, read-only query. Always bind
|
|
3
|
+
* user-supplied values through `options.params`/`options.types` — never
|
|
4
|
+
* string-interpolate them into `options.sql`. */
|
|
5
|
+
export declare function runQuery<T = Record<string, unknown>>(bq: BigQueryLike, options: RunQueryOptions): Promise<QueryPage<T>>;
|
|
6
|
+
/** Pages through every result of a query, yielding one row-array per page.
|
|
7
|
+
* Stops once BigQuery reports no further `nextPageToken`. */
|
|
8
|
+
export declare function runQueryAllPages<T = Record<string, unknown>>(bq: BigQueryLike, options: RunQueryOptions): AsyncGenerator<T[], void, void>;
|
|
9
|
+
/**
|
|
10
|
+
* Dry-runs a query to get its billed-byte estimate without actually running
|
|
11
|
+
* it (BigQuery does not execute a `dryRun: true` job). When
|
|
12
|
+
* `options.maxBytesBilled` is given, throws `DryRunBudgetExceededError`
|
|
13
|
+
* instead of returning once the estimate exceeds it — the caller-facing
|
|
14
|
+
* "don't let a user run an unbounded scan" guard.
|
|
15
|
+
*/
|
|
16
|
+
export declare function estimateQueryBytes(bq: BigQueryLike, options: EstimateQueryBytesOptions): Promise<DryRunEstimate>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameterised, paged querying + dry-run byte estimation against an injected
|
|
3
|
+
* BigQueryLike — the generic half of media-planner's inventoryWarehouse.ts
|
|
4
|
+
* (AK-11). Inventory-specific SQL stays in media-planner; this module only
|
|
5
|
+
* ever takes SQL + params from the caller and runs it.
|
|
6
|
+
*/
|
|
7
|
+
import { classifyBigQueryError } from "./errors.js";
|
|
8
|
+
import { DryRunBudgetExceededError } from "./errors.js";
|
|
9
|
+
/** Runs one page of a parameterised, read-only query. Always bind
|
|
10
|
+
* user-supplied values through `options.params`/`options.types` — never
|
|
11
|
+
* string-interpolate them into `options.sql`. */
|
|
12
|
+
export async function runQuery(bq, options) {
|
|
13
|
+
let job;
|
|
14
|
+
try {
|
|
15
|
+
[job] = await bq.createQueryJob({
|
|
16
|
+
query: options.sql,
|
|
17
|
+
params: options.params,
|
|
18
|
+
types: options.types,
|
|
19
|
+
labels: options.labels,
|
|
20
|
+
location: options.location,
|
|
21
|
+
maxResults: options.pageSize,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
throw classifyBigQueryError(err);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const [rows, next] = await job.getQueryResults({
|
|
29
|
+
maxResults: options.pageSize,
|
|
30
|
+
pageToken: options.pageToken,
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
rows,
|
|
34
|
+
jobId: job.id,
|
|
35
|
+
nextPageToken: next?.pageToken,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw classifyBigQueryError(err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Pages through every result of a query, yielding one row-array per page.
|
|
43
|
+
* Stops once BigQuery reports no further `nextPageToken`. */
|
|
44
|
+
export async function* runQueryAllPages(bq, options) {
|
|
45
|
+
let pageToken = options.pageToken;
|
|
46
|
+
for (;;) {
|
|
47
|
+
const page = await runQuery(bq, { ...options, pageToken });
|
|
48
|
+
yield page.rows;
|
|
49
|
+
if (!page.nextPageToken)
|
|
50
|
+
return;
|
|
51
|
+
pageToken = page.nextPageToken;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Dry-runs a query to get its billed-byte estimate without actually running
|
|
56
|
+
* it (BigQuery does not execute a `dryRun: true` job). When
|
|
57
|
+
* `options.maxBytesBilled` is given, throws `DryRunBudgetExceededError`
|
|
58
|
+
* instead of returning once the estimate exceeds it — the caller-facing
|
|
59
|
+
* "don't let a user run an unbounded scan" guard.
|
|
60
|
+
*/
|
|
61
|
+
export async function estimateQueryBytes(bq, options) {
|
|
62
|
+
let metadata;
|
|
63
|
+
try {
|
|
64
|
+
[, metadata] = await bq.createQueryJob({
|
|
65
|
+
query: options.sql,
|
|
66
|
+
params: options.params,
|
|
67
|
+
types: options.types,
|
|
68
|
+
location: options.location,
|
|
69
|
+
dryRun: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw classifyBigQueryError(err);
|
|
74
|
+
}
|
|
75
|
+
const stats = metadata.statistics?.query;
|
|
76
|
+
const estimate = {
|
|
77
|
+
totalBytesProcessed: Number(stats?.totalBytesProcessed ?? 0),
|
|
78
|
+
cacheHit: stats?.cacheHit ?? false,
|
|
79
|
+
};
|
|
80
|
+
if (options.maxBytesBilled !== undefined &&
|
|
81
|
+
estimate.totalBytesProcessed > options.maxBytesBilled) {
|
|
82
|
+
throw new DryRunBudgetExceededError(estimate.totalBytesProcessed, options.maxBytesBilled);
|
|
83
|
+
}
|
|
84
|
+
return estimate;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/bigquery/query.ts"],"names":[],"mappings":"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"}
|