@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.
Files changed (47) hide show
  1. package/README.md +6 -3
  2. package/dist/bigquery/client.d.ts +18 -0
  3. package/dist/bigquery/client.js +37 -0
  4. package/dist/bigquery/client.js.map +1 -0
  5. package/dist/bigquery/errors.d.ts +26 -0
  6. package/dist/bigquery/errors.js +80 -0
  7. package/dist/bigquery/errors.js.map +1 -0
  8. package/dist/bigquery/extract.d.ts +15 -0
  9. package/dist/bigquery/extract.js +88 -0
  10. package/dist/bigquery/extract.js.map +1 -0
  11. package/dist/bigquery/index.d.ts +47 -0
  12. package/dist/bigquery/index.js +46 -0
  13. package/dist/bigquery/index.js.map +1 -0
  14. package/dist/bigquery/labels.d.ts +16 -0
  15. package/dist/bigquery/labels.js +26 -0
  16. package/dist/bigquery/labels.js.map +1 -0
  17. package/dist/bigquery/query.d.ts +16 -0
  18. package/dist/bigquery/query.js +86 -0
  19. package/dist/bigquery/query.js.map +1 -0
  20. package/dist/bigquery/types.d.ts +155 -0
  21. package/dist/bigquery/types.js +15 -0
  22. package/dist/bigquery/types.js.map +1 -0
  23. package/dist/chat/callback-secret.d.ts +21 -0
  24. package/dist/chat/callback-secret.js +33 -0
  25. package/dist/chat/callback-secret.js.map +1 -0
  26. package/dist/chat/env.d.ts +27 -0
  27. package/dist/chat/env.js +44 -0
  28. package/dist/chat/env.js.map +1 -0
  29. package/dist/chat/index.d.ts +72 -0
  30. package/dist/chat/index.js +70 -0
  31. package/dist/chat/index.js.map +1 -0
  32. package/dist/chat/launcher.d.ts +43 -0
  33. package/dist/chat/launcher.js +103 -0
  34. package/dist/chat/launcher.js.map +1 -0
  35. package/dist/chat/mcp-protocol.d.ts +96 -0
  36. package/dist/chat/mcp-protocol.js +200 -0
  37. package/dist/chat/mcp-protocol.js.map +1 -0
  38. package/dist/chat/mcp-server.d.ts +17 -0
  39. package/dist/chat/mcp-server.js +71 -0
  40. package/dist/chat/mcp-server.js.map +1 -0
  41. package/dist/chat/types.d.ts +91 -0
  42. package/dist/chat/types.js +5 -0
  43. package/dist/chat/types.js.map +1 -0
  44. package/package.json +10 -1
  45. package/scripts/pending-release-count.mjs +37 -0
  46. package/scripts/release.sh +126 -0
  47. 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
- Two subpath modules, one npm package:
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
- Auth is a token in `~/.npmrc` (`//registry.npmjs.org/:_authToken=...`) — no CI publish
29
- workflow yet; that's still open (see the artifact's Needs Attention tab).
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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"}