@adcp/sdk 15.5.0 → 15.6.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 +15 -0
- package/dist/lib/auth/oauth/index.d.mts +1 -0
- package/dist/lib/auth/oauth/index.d.ts +1 -0
- package/dist/lib/auth/oauth/index.js +5 -0
- package/dist/lib/auth/oauth/index.mjs +3 -0
- package/dist/lib/auth/oauth/pending-web-flow.d.mts +16 -0
- package/dist/lib/auth/oauth/pending-web-flow.d.ts +16 -0
- package/dist/lib/auth/oauth/pending-web-flow.js +66 -0
- package/dist/lib/auth/oauth/pending-web-flow.mjs +41 -0
- package/dist/lib/auth/oauth/web-flow.js +3 -3
- package/dist/lib/auth/oauth/web-flow.mjs +3 -3
- package/dist/lib/registry/types.generated.d.mts +63 -1
- package/dist/lib/registry/types.generated.d.ts +63 -1
- package/dist/lib/reporting/azure/index.d.mts +26 -0
- package/dist/lib/reporting/azure/index.d.ts +26 -0
- package/dist/lib/reporting/azure/index.js +247 -0
- package/dist/lib/reporting/azure/index.mjs +230 -0
- package/dist/lib/reporting/bigquery/index.d.mts +127 -0
- package/dist/lib/reporting/bigquery/index.d.ts +127 -0
- package/dist/lib/reporting/bigquery/index.js +379 -0
- package/dist/lib/reporting/bigquery/index.mjs +341 -0
- package/dist/lib/reporting/ledger/postgres.d.mts +10 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +10 -0
- package/dist/lib/reporting/ledger/postgres.js +23 -2
- package/dist/lib/reporting/ledger/postgres.mjs +23 -2
- package/dist/lib/reporting/ledger/row-storage-object.js +31 -8
- package/dist/lib/reporting/ledger/row-storage-object.mjs +32 -9
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +0 -7
- package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +0 -7
- package/dist/lib/reporting/ledger/row-storage-provider-io.js +0 -10
- package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +0 -8
- package/dist/lib/reporting/s3/index.d.mts +29 -0
- package/dist/lib/reporting/s3/index.d.ts +29 -0
- package/dist/lib/reporting/s3/index.js +324 -0
- package/dist/lib/reporting/s3/index.mjs +317 -0
- package/dist/lib/reporting/service/index.d.mts +3 -1
- package/dist/lib/reporting/service/index.d.ts +3 -1
- package/dist/lib/reporting/service/index.js +1 -0
- package/dist/lib/reporting/service/index.mjs +1 -0
- package/dist/lib/schemas/browser.d.mts +14 -0
- package/dist/lib/schemas/browser.d.ts +14 -0
- package/dist/lib/schemas/browser.js +48 -0
- package/dist/lib/schemas/browser.mjs +20 -0
- package/dist/lib/schemas/index.d.mts +2 -120
- package/dist/lib/schemas/index.d.ts +2 -120
- package/dist/lib/schemas/index.js +3 -11
- package/dist/lib/schemas/index.mjs +1 -9
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/index.d.mts +1 -0
- package/dist/lib/testing/index.d.ts +1 -0
- package/dist/lib/testing/index.js +3 -0
- package/dist/lib/testing/index.mjs +2 -0
- package/dist/lib/testing/pending-web-flow-store.d.mts +20 -0
- package/dist/lib/testing/pending-web-flow-store.d.ts +20 -0
- package/dist/lib/testing/pending-web-flow-store.js +132 -0
- package/dist/lib/testing/pending-web-flow-store.mjs +98 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/validation/get-products-request.d.mts +122 -0
- package/dist/lib/validation/get-products-request.d.ts +122 -0
- package/dist/lib/validation/get-products-request.js +39 -0
- package/dist/lib/validation/get-products-request.mjs +14 -0
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +1 -1
- package/docs/ZOD-SCHEMAS.md +13 -1
- package/docs/guides/OAUTH-ISSUER-BINDING.md +6 -0
- package/docs/guides/REPORTING-LEDGER.md +165 -0
- package/docs/guides/REPORTING-OPERATIONS.md +76 -1
- package/docs/guides/WEB-OAUTH.md +48 -1
- package/docs/llms.txt +1 -1
- package/docs/migration-14.x-rc-worksheet.md +6 -4
- 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
|
+
});
|