@adcp/sdk 15.5.0 → 15.7.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 (81) hide show
  1. package/README.md +15 -0
  2. package/dist/lib/auth/oauth/index.d.mts +1 -0
  3. package/dist/lib/auth/oauth/index.d.ts +1 -0
  4. package/dist/lib/auth/oauth/index.js +5 -0
  5. package/dist/lib/auth/oauth/index.mjs +3 -0
  6. package/dist/lib/auth/oauth/pending-web-flow.d.mts +16 -0
  7. package/dist/lib/auth/oauth/pending-web-flow.d.ts +16 -0
  8. package/dist/lib/auth/oauth/pending-web-flow.js +66 -0
  9. package/dist/lib/auth/oauth/pending-web-flow.mjs +41 -0
  10. package/dist/lib/auth/oauth/web-flow.js +3 -3
  11. package/dist/lib/auth/oauth/web-flow.mjs +3 -3
  12. package/dist/lib/registry/types.generated.d.mts +63 -1
  13. package/dist/lib/registry/types.generated.d.ts +63 -1
  14. package/dist/lib/reporting/azure/index.d.mts +26 -0
  15. package/dist/lib/reporting/azure/index.d.ts +26 -0
  16. package/dist/lib/reporting/azure/index.js +247 -0
  17. package/dist/lib/reporting/azure/index.mjs +230 -0
  18. package/dist/lib/reporting/bigquery/index.d.mts +127 -0
  19. package/dist/lib/reporting/bigquery/index.d.ts +127 -0
  20. package/dist/lib/reporting/bigquery/index.js +379 -0
  21. package/dist/lib/reporting/bigquery/index.mjs +341 -0
  22. package/dist/lib/reporting/ledger/handler.js +20 -8
  23. package/dist/lib/reporting/ledger/handler.mjs +20 -8
  24. package/dist/lib/reporting/ledger/postgres.d.mts +21 -0
  25. package/dist/lib/reporting/ledger/postgres.d.ts +21 -0
  26. package/dist/lib/reporting/ledger/postgres.js +35 -2
  27. package/dist/lib/reporting/ledger/postgres.mjs +35 -2
  28. package/dist/lib/reporting/ledger/row-storage-object.js +31 -8
  29. package/dist/lib/reporting/ledger/row-storage-object.mjs +32 -9
  30. package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +0 -7
  31. package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +0 -7
  32. package/dist/lib/reporting/ledger/row-storage-provider-io.js +0 -10
  33. package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +0 -8
  34. package/dist/lib/reporting/s3/index.d.mts +29 -0
  35. package/dist/lib/reporting/s3/index.d.ts +29 -0
  36. package/dist/lib/reporting/s3/index.js +324 -0
  37. package/dist/lib/reporting/s3/index.mjs +317 -0
  38. package/dist/lib/reporting/service/index.d.mts +39 -22
  39. package/dist/lib/reporting/service/index.d.ts +39 -22
  40. package/dist/lib/reporting/service/index.js +75 -64
  41. package/dist/lib/reporting/service/index.mjs +79 -63
  42. package/dist/lib/reporting/service/maintenance.d.mts +91 -0
  43. package/dist/lib/reporting/service/maintenance.d.ts +91 -0
  44. package/dist/lib/reporting/service/maintenance.js +140 -0
  45. package/dist/lib/reporting/service/maintenance.mjs +114 -0
  46. package/dist/lib/schemas/browser.d.mts +14 -0
  47. package/dist/lib/schemas/browser.d.ts +14 -0
  48. package/dist/lib/schemas/browser.js +48 -0
  49. package/dist/lib/schemas/browser.mjs +20 -0
  50. package/dist/lib/schemas/index.d.mts +2 -120
  51. package/dist/lib/schemas/index.d.ts +2 -120
  52. package/dist/lib/schemas/index.js +3 -11
  53. package/dist/lib/schemas/index.mjs +1 -9
  54. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  55. package/dist/lib/testing/index.d.mts +1 -0
  56. package/dist/lib/testing/index.d.ts +1 -0
  57. package/dist/lib/testing/index.js +3 -0
  58. package/dist/lib/testing/index.mjs +2 -0
  59. package/dist/lib/testing/pending-web-flow-store.d.mts +20 -0
  60. package/dist/lib/testing/pending-web-flow-store.d.ts +20 -0
  61. package/dist/lib/testing/pending-web-flow-store.js +132 -0
  62. package/dist/lib/testing/pending-web-flow-store.mjs +98 -0
  63. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  64. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  65. package/dist/lib/validation/get-products-request.d.mts +122 -0
  66. package/dist/lib/validation/get-products-request.d.ts +122 -0
  67. package/dist/lib/validation/get-products-request.js +39 -0
  68. package/dist/lib/validation/get-products-request.mjs +14 -0
  69. package/dist/lib/version.d.mts +3 -3
  70. package/dist/lib/version.d.ts +3 -3
  71. package/dist/lib/version.js +3 -3
  72. package/dist/lib/version.mjs +3 -3
  73. package/docs/TYPE-SUMMARY.md +1 -1
  74. package/docs/ZOD-SCHEMAS.md +13 -1
  75. package/docs/guides/OAUTH-ISSUER-BINDING.md +6 -0
  76. package/docs/guides/REPORTING-LEDGER.md +254 -0
  77. package/docs/guides/REPORTING-OPERATIONS.md +82 -1
  78. package/docs/guides/WEB-OAUTH.md +48 -1
  79. package/docs/llms.txt +1 -1
  80. package/docs/migration-14.x-rc-worksheet.md +6 -4
  81. package/package.json +64 -1
@@ -0,0 +1,127 @@
1
+ import type { PostgresReportingLedgerStore, ReportingLedgerRevisionMetadataV1, ReportingPgPool } from '../ledger';
2
+ /**
3
+ * BigQuery warehouse sink for Reliable Reporting (shared SDK persistence spec,
4
+ * adcontextprotocol/adcp#7996 §3.6).
5
+ *
6
+ * The sink is a derived, non-authoritative copy of committed revisions. It
7
+ * follows the ledger's host change feed, reads every revision's rows through
8
+ * the store's verified reader, maps them to the adopter's columns, and loads
9
+ * one batch per tick with a deterministic job ID. It is never read for
10
+ * serving or verification, so its tables, columns, partitioning and retention
11
+ * are the adopter's.
12
+ *
13
+ * Exactly-once loading: a batch (change-feed range, revision list, job IDs) is
14
+ * planned durably in PostgreSQL before any load. A crash between the load and
15
+ * the cursor commit replays the same batch under the same job IDs, which
16
+ * BigQuery refuses to run twice. A failed load job is retried under a new
17
+ * attempt suffix only after BigQuery reports that job finished with an error,
18
+ * and each table keeps its own attempt count, so no batch lands twice.
19
+ */
20
+ /** Apply after `REPORTING_LEDGER_CHANGES_MIGRATION`. Holds one row per sink. */
21
+ export declare const REPORTING_WAREHOUSE_SINK_MIGRATION: string;
22
+ /** Structural subset of the official `@google-cloud/bigquery` client the sink uses. */
23
+ export interface ReportingBigQueryClientV1 {
24
+ dataset(datasetId: string, options?: {
25
+ projectId?: string;
26
+ location?: string;
27
+ }): {
28
+ table(tableId: string): {
29
+ load(source: string, metadata: Record<string, unknown>): Promise<unknown>;
30
+ };
31
+ };
32
+ job(jobId: string, options?: {
33
+ location?: string;
34
+ }): {
35
+ getMetadata(): Promise<unknown>;
36
+ };
37
+ query(options: {
38
+ query: string;
39
+ location?: string;
40
+ }): Promise<unknown>;
41
+ }
42
+ export interface ReportingWarehouseRowContextV1 {
43
+ revision: ReportingLedgerRevisionMetadataV1;
44
+ ordinal: number;
45
+ }
46
+ export interface CreateBigQueryReportingWarehouseSinkOptionsV1 {
47
+ /** Ledger store with `changeFeed: true`. */
48
+ store: Pick<PostgresReportingLedgerStore, 'changesAfter' | 'saveFeedConsumerCursor' | 'getRevisionMetadata' | 'readRevisionRows'>;
49
+ /** PostgreSQL pool holding `REPORTING_WAREHOUSE_SINK_MIGRATION`. */
50
+ db: Pick<ReportingPgPool, 'query'>;
51
+ /** Host-constructed official BigQuery client. */
52
+ bigquery: ReportingBigQueryClientV1;
53
+ /** Stable sink name; also the registered change-feed consumer name. */
54
+ name: string;
55
+ projectId?: string;
56
+ datasetId: string;
57
+ rowsTableId: string;
58
+ revisionsTableId: string;
59
+ /** Dataset location, so jobs run in the dataset's region. */
60
+ location?: string;
61
+ /** Restrict the sink to one account; omit for the deployment-wide feed. */
62
+ account_id?: string;
63
+ /**
64
+ * Map one verified report row to the adopter's columns. The sink always adds
65
+ * `reporting_revision_id`, `reporting_obligation_id`, `account_id`,
66
+ * `revision_number`, `finality`, `period_date` and `ordinal`. Default: the
67
+ * row as a `row` JSON column.
68
+ */
69
+ mapRow?(row: Record<string, unknown>, context: ReportingWarehouseRowContextV1): Record<string, unknown>;
70
+ /** Revisions per batch. Default 200. */
71
+ maxRevisionsPerBatch?: number;
72
+ /** Job labels for cost attribution. */
73
+ labels?: Readonly<Record<string, string>>;
74
+ /**
75
+ * Deadline for one load (and its status check). Default 10 minutes. It is
76
+ * also sent as the job's `jobTimeoutMs`. A run that times out leaves its
77
+ * plan in place; the next run reuses the same job ID and waits for, or
78
+ * accepts, the job BigQuery is still running.
79
+ */
80
+ loadTimeoutMilliseconds?: number;
81
+ }
82
+ export interface BigQueryReportingWarehouseSinkV1 {
83
+ /**
84
+ * Load at most one batch. Resolves with what was loaded; `idle` when nothing new committed.
85
+ *
86
+ * `missing` lists revision IDs the change feed announced that no longer
87
+ * exist in the ledger because their period was retired before this batch
88
+ * ran. That is a data-loss signal: the sink fell behind retention (it was
89
+ * down longer than `maxFeedHoldDays`, so retention stopped waiting for it)
90
+ * and the warehouse lacks those revisions. Backfill them from another source
91
+ * or accept the gap. The cursor still advances so the sink is not stuck.
92
+ * Alert on a non-empty `missing`. After a crash-and-replay of a planned
93
+ * batch, a listed revision may already have been loaded by the first attempt.
94
+ */
95
+ runOnce(options?: {
96
+ signal?: AbortSignal;
97
+ }): Promise<{
98
+ idle: boolean;
99
+ revisions: number;
100
+ rows: number;
101
+ missing: string[];
102
+ }>;
103
+ /** DDL for the default rows and revisions tables (partitioned and clustered). */
104
+ readonly defaultTablesSql: string;
105
+ /** DDL for the reference `current_rows` view. */
106
+ readonly currentRowsViewSql: string;
107
+ }
108
+ export declare function createBigQueryReportingWarehouseSinkV1(options: CreateBigQueryReportingWarehouseSinkOptionsV1): BigQueryReportingWarehouseSinkV1;
109
+ /** Deterministic load job ID for one planned change-feed range. */
110
+ export declare function reportingWarehouseJobIdV1(name: string, from: string | null, to: string): string;
111
+ interface WarehouseTablesV1 {
112
+ projectId?: string;
113
+ datasetId: string;
114
+ rowsTableId: string;
115
+ revisionsTableId: string;
116
+ }
117
+ /** DDL for the default warehouse tables: partitioned by period date, clustered by account and revision. */
118
+ export declare function reportingWarehouseDefaultTablesSqlV1(tables: WarehouseTablesV1): string;
119
+ /**
120
+ * DDL for the reference `current_rows` view: only each obligation's current
121
+ * revision (its official revision, otherwise its latest snapshot). Summing
122
+ * cumulative snapshots across revisions double-counts; query this view.
123
+ */
124
+ export declare function reportingWarehouseCurrentRowsViewSqlV1(tables: WarehouseTablesV1 & {
125
+ viewId?: string;
126
+ }): string;
127
+ export {};
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var bigquery_exports = {};
30
+ __export(bigquery_exports, {
31
+ REPORTING_WAREHOUSE_SINK_MIGRATION: () => REPORTING_WAREHOUSE_SINK_MIGRATION,
32
+ createBigQueryReportingWarehouseSinkV1: () => createBigQueryReportingWarehouseSinkV1,
33
+ reportingWarehouseCurrentRowsViewSqlV1: () => reportingWarehouseCurrentRowsViewSqlV1,
34
+ reportingWarehouseDefaultTablesSqlV1: () => reportingWarehouseDefaultTablesSqlV1,
35
+ reportingWarehouseJobIdV1: () => reportingWarehouseJobIdV1
36
+ });
37
+ module.exports = __toCommonJS(bigquery_exports);
38
+ var import_crypto = require("crypto");
39
+ var import_fs = require("fs");
40
+ var import_os = require("os");
41
+ var path = __toESM(require("path"));
42
+ const REPORTING_WAREHOUSE_SINK_MIGRATION = `
43
+ CREATE TABLE IF NOT EXISTS adcp_reporting_warehouse_sinks (
44
+ sink_name TEXT PRIMARY KEY CHECK (sink_name ~ '^[A-Za-z0-9_.:-]{1,128}$'),
45
+ committed_cursor TEXT,
46
+ planned_from_cursor TEXT,
47
+ planned_to_cursor TEXT,
48
+ planned_revisions JSONB,
49
+ planned_rows_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_attempt >= 0),
50
+ planned_revisions_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_attempt >= 0),
51
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
52
+ CHECK ((planned_to_cursor IS NULL) = (planned_revisions IS NULL))
53
+ );
54
+ `.trim();
55
+ const PROJECT = /^[a-z][a-z0-9-]{4,61}[a-z0-9]$/;
56
+ const DATASET_OR_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,1023}$/;
57
+ const NAME = /^[A-Za-z0-9_.:-]{1,128}$/;
58
+ function createBigQueryReportingWarehouseSinkV1(options) {
59
+ if (!NAME.test(options.name)) throw new TypeError("Warehouse sink name must be a bounded non-secret identifier");
60
+ if (options.projectId !== void 0 && !PROJECT.test(options.projectId)) {
61
+ throw new TypeError("BigQuery projectId is invalid");
62
+ }
63
+ for (const [label, value] of [
64
+ ["datasetId", options.datasetId],
65
+ ["rowsTableId", options.rowsTableId],
66
+ ["revisionsTableId", options.revisionsTableId]
67
+ ]) {
68
+ if (!DATASET_OR_TABLE.test(value)) throw new TypeError(`BigQuery ${label} is invalid`);
69
+ }
70
+ const loadTimeout = options.loadTimeoutMilliseconds ?? 6e5;
71
+ if (!Number.isSafeInteger(loadTimeout) || loadTimeout < 1e3 || loadTimeout > 6 * 36e5) {
72
+ throw new RangeError("loadTimeoutMilliseconds must be between 1 second and 6 hours");
73
+ }
74
+ const maxRevisions = options.maxRevisionsPerBatch ?? 200;
75
+ if (!Number.isSafeInteger(maxRevisions) || maxRevisions < 1 || maxRevisions > 1e4) {
76
+ throw new RangeError("maxRevisionsPerBatch must be between 1 and 10000");
77
+ }
78
+ const tables = {
79
+ projectId: options.projectId,
80
+ datasetId: options.datasetId,
81
+ rowsTableId: options.rowsTableId,
82
+ revisionsTableId: options.revisionsTableId
83
+ };
84
+ const dataset = options.bigquery.dataset(options.datasetId, {
85
+ ...options.projectId ? { projectId: options.projectId } : {},
86
+ ...options.location ? { location: options.location } : {}
87
+ });
88
+ async function loadOnce(tableId, jobId, lines, signal) {
89
+ signal?.throwIfAborted();
90
+ const directory = await import_fs.promises.mkdtemp(path.join((0, import_os.tmpdir)(), "adcp-warehouse-"));
91
+ const file = path.join(directory, `${jobId}.ndjson`);
92
+ try {
93
+ await import_fs.promises.writeFile(file, lines.length ? lines.join("\n") + "\n" : "");
94
+ try {
95
+ await withinDeadline(
96
+ () => dataset.table(tableId).load(file, {
97
+ jobId,
98
+ sourceFormat: "NEWLINE_DELIMITED_JSON",
99
+ writeDisposition: "WRITE_APPEND",
100
+ jobTimeoutMs: String(loadTimeout),
101
+ ...options.location ? { location: options.location } : {},
102
+ ...options.labels ? { labels: { ...options.labels } } : {}
103
+ }),
104
+ loadTimeout,
105
+ signal
106
+ );
107
+ } catch (error) {
108
+ if (!isAlreadyExists(error)) throw error;
109
+ const metadata = await withinDeadline(
110
+ () => options.bigquery.job(jobId, options.location ? { location: options.location } : void 0).getMetadata(),
111
+ loadTimeout,
112
+ signal
113
+ );
114
+ assertJobSucceeded(metadata);
115
+ }
116
+ } finally {
117
+ await import_fs.promises.rm(directory, { recursive: true, force: true });
118
+ }
119
+ }
120
+ return {
121
+ defaultTablesSql: reportingWarehouseDefaultTablesSqlV1(tables),
122
+ currentRowsViewSql: reportingWarehouseCurrentRowsViewSqlV1(tables),
123
+ async runOnce(runOptions = {}) {
124
+ const signal = runOptions.signal;
125
+ await options.db.query(
126
+ `INSERT INTO adcp_reporting_warehouse_sinks (sink_name) VALUES ($1) ON CONFLICT DO NOTHING`,
127
+ [options.name]
128
+ );
129
+ const state = (await options.db.query(
130
+ `SELECT committed_cursor, planned_from_cursor, planned_to_cursor, planned_revisions,
131
+ planned_rows_attempt, planned_revisions_attempt
132
+ FROM adcp_reporting_warehouse_sinks WHERE sink_name = $1`,
133
+ [options.name]
134
+ )).rows[0];
135
+ let plan = state.planned_to_cursor && state.planned_revisions ? {
136
+ from: state.planned_from_cursor,
137
+ to: state.planned_to_cursor,
138
+ revisions: state.planned_revisions,
139
+ rowsAttempt: state.planned_rows_attempt,
140
+ revisionsAttempt: state.planned_revisions_attempt
141
+ } : void 0;
142
+ if (!plan) {
143
+ const page = await options.store.changesAfter({
144
+ ...state.committed_cursor ? { cursor: state.committed_cursor } : {},
145
+ ...options.account_id ? { account_id: options.account_id } : {},
146
+ kinds: ["revision"],
147
+ limit: maxRevisions
148
+ });
149
+ if (page.records.length === 0) {
150
+ if (page.cursor !== state.committed_cursor) await commit(page.cursor);
151
+ return { idle: true, revisions: 0, rows: 0, missing: [] };
152
+ }
153
+ plan = {
154
+ from: state.committed_cursor,
155
+ to: page.cursor,
156
+ revisions: page.records.map((record) => ({ id: record.record_id, account_id: record.account_id })),
157
+ rowsAttempt: 0,
158
+ revisionsAttempt: 0
159
+ };
160
+ await options.db.query(
161
+ `UPDATE adcp_reporting_warehouse_sinks
162
+ SET planned_from_cursor = $2, planned_to_cursor = $3, planned_revisions = $4::jsonb,
163
+ planned_rows_attempt = 0, planned_revisions_attempt = 0, updated_at = clock_timestamp()
164
+ WHERE sink_name = $1`,
165
+ [options.name, plan.from, plan.to, JSON.stringify(plan.revisions)]
166
+ );
167
+ }
168
+ const rowLines = [];
169
+ const revisionLines = [];
170
+ const missing = [];
171
+ for (const planned of plan.revisions) {
172
+ signal?.throwIfAborted();
173
+ const revision = await options.store.getRevisionMetadata(planned.id, planned.account_id);
174
+ if (!revision) {
175
+ missing.push(planned.id);
176
+ continue;
177
+ }
178
+ const periodDate = sourceLocalDate(
179
+ revision.wireRevision.period?.start,
180
+ revision.wireRevision.period?.source_timezone
181
+ );
182
+ const base = {
183
+ reporting_revision_id: revision.reporting_revision_id,
184
+ reporting_obligation_id: revision.reporting_obligation_id,
185
+ account_id: planned.account_id,
186
+ revision_number: revision.revisionNumber,
187
+ finality: revision.finality,
188
+ period_date: periodDate
189
+ };
190
+ for (let offset = 0; offset < revision.binding.rowCount; ) {
191
+ const page = await options.store.readRevisionRows({
192
+ reporting_revision_id: revision.reporting_revision_id,
193
+ account_id: planned.account_id,
194
+ offset,
195
+ limit: 5e3,
196
+ ...signal ? { signal } : {}
197
+ });
198
+ if (!page || page.rows.length === 0) throw new Error("Reporting revision rows ended early");
199
+ page.rows.forEach((row, index) => {
200
+ const ordinal = offset + index;
201
+ const mapped = options.mapRow ? options.mapRow(row, { revision, ordinal }) : { row };
202
+ rowLines.push(JSON.stringify({ ...mapped, ...base, ordinal }));
203
+ });
204
+ offset += page.rows.length;
205
+ }
206
+ revisionLines.push(
207
+ JSON.stringify({
208
+ ...base,
209
+ supersedes_reporting_revision_id: revision.supersedes_reporting_revision_id ?? null,
210
+ period_start: revision.wireRevision.period?.start ?? null,
211
+ period_end: revision.wireRevision.period?.end ?? null,
212
+ source_timezone: revision.wireRevision.period?.source_timezone ?? null,
213
+ observed_at: revision.observedAt,
214
+ data_through: revision.dataThrough,
215
+ revision_content_sha256: revision.binding.sha256,
216
+ row_count: revision.binding.rowCount,
217
+ control_totals: revision.wireRevision.control_totals ?? []
218
+ })
219
+ );
220
+ }
221
+ const baseJobId = reportingWarehouseJobIdV1(options.name, plan.from, plan.to);
222
+ const suffix = (attempt) => attempt ? `_a${attempt}` : "";
223
+ const steps = [
224
+ { table: options.rowsTableId, kind: "rows", lines: rowLines, attempt: plan.rowsAttempt },
225
+ { table: options.revisionsTableId, kind: "revs", lines: revisionLines, attempt: plan.revisionsAttempt }
226
+ ];
227
+ for (const step of steps) {
228
+ if (!step.lines.length) continue;
229
+ try {
230
+ await loadOnce(step.table, `${baseJobId}_${step.kind}${suffix(step.attempt)}`, [...step.lines], signal);
231
+ } catch (error) {
232
+ if (isFinishedWithError(error)) {
233
+ const column = step.kind === "rows" ? "planned_rows_attempt" : "planned_revisions_attempt";
234
+ await options.db.query(
235
+ `UPDATE adcp_reporting_warehouse_sinks SET ${column} = ${column} + 1,
236
+ updated_at = clock_timestamp() WHERE sink_name = $1`,
237
+ [options.name]
238
+ );
239
+ }
240
+ throw error;
241
+ }
242
+ }
243
+ await commit(plan.to);
244
+ return { idle: false, revisions: revisionLines.length, rows: rowLines.length, missing };
245
+ }
246
+ };
247
+ async function commit(cursor) {
248
+ await options.db.query(
249
+ `UPDATE adcp_reporting_warehouse_sinks
250
+ SET committed_cursor = $2, planned_from_cursor = NULL, planned_to_cursor = NULL,
251
+ planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
252
+ updated_at = clock_timestamp()
253
+ WHERE sink_name = $1`,
254
+ [options.name, cursor]
255
+ );
256
+ await options.store.saveFeedConsumerCursor(options.name, cursor);
257
+ }
258
+ }
259
+ function reportingWarehouseJobIdV1(name, from, to) {
260
+ return `adcp_rr_${(0, import_crypto.createHash)("sha256").update(JSON.stringify([name, from, to])).digest("hex").slice(0, 40)}`;
261
+ }
262
+ function qualified(tables, table) {
263
+ return `\`${tables.projectId ? `${tables.projectId}.` : ""}${tables.datasetId}.${table}\``;
264
+ }
265
+ function reportingWarehouseDefaultTablesSqlV1(tables) {
266
+ return `
267
+ CREATE TABLE IF NOT EXISTS ${qualified(tables, tables.rowsTableId)} (
268
+ reporting_revision_id STRING NOT NULL,
269
+ reporting_obligation_id STRING NOT NULL,
270
+ account_id STRING NOT NULL,
271
+ revision_number INT64 NOT NULL,
272
+ finality STRING NOT NULL,
273
+ period_date DATE,
274
+ ordinal INT64 NOT NULL,
275
+ row JSON
276
+ )
277
+ PARTITION BY period_date
278
+ CLUSTER BY account_id, reporting_revision_id;
279
+
280
+ CREATE TABLE IF NOT EXISTS ${qualified(tables, tables.revisionsTableId)} (
281
+ reporting_revision_id STRING NOT NULL,
282
+ reporting_obligation_id STRING NOT NULL,
283
+ account_id STRING NOT NULL,
284
+ revision_number INT64 NOT NULL,
285
+ finality STRING NOT NULL,
286
+ period_date DATE,
287
+ supersedes_reporting_revision_id STRING,
288
+ period_start TIMESTAMP,
289
+ period_end TIMESTAMP,
290
+ source_timezone STRING,
291
+ observed_at TIMESTAMP,
292
+ data_through TIMESTAMP,
293
+ revision_content_sha256 STRING NOT NULL,
294
+ row_count INT64 NOT NULL,
295
+ control_totals JSON
296
+ )
297
+ PARTITION BY period_date
298
+ CLUSTER BY account_id, reporting_obligation_id;`.trim();
299
+ }
300
+ function reportingWarehouseCurrentRowsViewSqlV1(tables) {
301
+ const view = tables.viewId ?? "current_rows";
302
+ if (!DATASET_OR_TABLE.test(view)) throw new TypeError("BigQuery viewId is invalid");
303
+ return `
304
+ CREATE OR REPLACE VIEW ${qualified(tables, view)} AS
305
+ SELECT row_data.*
306
+ FROM ${qualified(tables, tables.rowsTableId)} AS row_data
307
+ JOIN (
308
+ SELECT reporting_obligation_id,
309
+ ARRAY_AGG(reporting_revision_id ORDER BY revision_number DESC LIMIT 1)[OFFSET(0)] AS reporting_revision_id
310
+ FROM ${qualified(tables, tables.revisionsTableId)}
311
+ GROUP BY reporting_obligation_id
312
+ ) AS current_revision
313
+ USING (reporting_obligation_id, reporting_revision_id);`.trim();
314
+ }
315
+ function sourceLocalDate(start, timeZone) {
316
+ if (!start) return null;
317
+ const instant = new Date(start);
318
+ if (Number.isNaN(instant.getTime())) return null;
319
+ try {
320
+ const parts = new Intl.DateTimeFormat("en-CA", {
321
+ timeZone: timeZone ?? "UTC",
322
+ year: "numeric",
323
+ month: "2-digit",
324
+ day: "2-digit"
325
+ }).format(instant);
326
+ return parts;
327
+ } catch {
328
+ return instant.toISOString().slice(0, 10);
329
+ }
330
+ }
331
+ async function withinDeadline(operation, milliseconds, signal) {
332
+ signal?.throwIfAborted();
333
+ let timer;
334
+ let onAbort;
335
+ try {
336
+ return await Promise.race([
337
+ operation(),
338
+ new Promise((_, reject) => {
339
+ timer = setTimeout(
340
+ () => reject(new Error("BigQuery call exceeded its deadline; the batch will be retried")),
341
+ milliseconds
342
+ );
343
+ if (signal) {
344
+ onAbort = () => reject(signal.reason ?? new Error("aborted"));
345
+ signal.addEventListener("abort", onAbort, { once: true });
346
+ }
347
+ })
348
+ ]);
349
+ } finally {
350
+ if (timer) clearTimeout(timer);
351
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
352
+ }
353
+ }
354
+ function isAlreadyExists(error) {
355
+ const record = error;
356
+ return record?.code === 409 || !!record?.errors?.some((entry) => entry.reason === "duplicate");
357
+ }
358
+ function isFinishedWithError(error) {
359
+ return error?.adcpJobFailed === true;
360
+ }
361
+ function assertJobSucceeded(metadata) {
362
+ const record = Array.isArray(metadata) ? metadata[0] : metadata;
363
+ if (record?.status?.state !== "DONE") {
364
+ throw new Error("A BigQuery load job for this batch is still running; retry later");
365
+ }
366
+ if (record.status.errorResult) {
367
+ const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
368
+ Object.defineProperty(failure, "adcpJobFailed", { value: true });
369
+ throw failure;
370
+ }
371
+ }
372
+ // Annotate the CommonJS export names for ESM import in node:
373
+ 0 && (module.exports = {
374
+ REPORTING_WAREHOUSE_SINK_MIGRATION,
375
+ createBigQueryReportingWarehouseSinkV1,
376
+ reportingWarehouseCurrentRowsViewSqlV1,
377
+ reportingWarehouseDefaultTablesSqlV1,
378
+ reportingWarehouseJobIdV1
379
+ });