@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.
- 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/handler.js +20 -8
- package/dist/lib/reporting/ledger/handler.mjs +20 -8
- package/dist/lib/reporting/ledger/postgres.d.mts +21 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +21 -0
- package/dist/lib/reporting/ledger/postgres.js +35 -2
- package/dist/lib/reporting/ledger/postgres.mjs +35 -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 +39 -22
- package/dist/lib/reporting/service/index.d.ts +39 -22
- package/dist/lib/reporting/service/index.js +75 -64
- package/dist/lib/reporting/service/index.mjs +79 -63
- package/dist/lib/reporting/service/maintenance.d.mts +91 -0
- package/dist/lib/reporting/service/maintenance.d.ts +91 -0
- package/dist/lib/reporting/service/maintenance.js +140 -0
- package/dist/lib/reporting/service/maintenance.mjs +114 -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 +254 -0
- package/docs/guides/REPORTING-OPERATIONS.md +82 -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,341 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
const REPORTING_WAREHOUSE_SINK_MIGRATION = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS adcp_reporting_warehouse_sinks (
|
|
7
|
+
sink_name TEXT PRIMARY KEY CHECK (sink_name ~ '^[A-Za-z0-9_.:-]{1,128}$'),
|
|
8
|
+
committed_cursor TEXT,
|
|
9
|
+
planned_from_cursor TEXT,
|
|
10
|
+
planned_to_cursor TEXT,
|
|
11
|
+
planned_revisions JSONB,
|
|
12
|
+
planned_rows_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_rows_attempt >= 0),
|
|
13
|
+
planned_revisions_attempt INTEGER NOT NULL DEFAULT 0 CHECK (planned_revisions_attempt >= 0),
|
|
14
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
|
|
15
|
+
CHECK ((planned_to_cursor IS NULL) = (planned_revisions IS NULL))
|
|
16
|
+
);
|
|
17
|
+
`.trim();
|
|
18
|
+
const PROJECT = /^[a-z][a-z0-9-]{4,61}[a-z0-9]$/;
|
|
19
|
+
const DATASET_OR_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,1023}$/;
|
|
20
|
+
const NAME = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
21
|
+
function createBigQueryReportingWarehouseSinkV1(options) {
|
|
22
|
+
if (!NAME.test(options.name)) throw new TypeError("Warehouse sink name must be a bounded non-secret identifier");
|
|
23
|
+
if (options.projectId !== void 0 && !PROJECT.test(options.projectId)) {
|
|
24
|
+
throw new TypeError("BigQuery projectId is invalid");
|
|
25
|
+
}
|
|
26
|
+
for (const [label, value] of [
|
|
27
|
+
["datasetId", options.datasetId],
|
|
28
|
+
["rowsTableId", options.rowsTableId],
|
|
29
|
+
["revisionsTableId", options.revisionsTableId]
|
|
30
|
+
]) {
|
|
31
|
+
if (!DATASET_OR_TABLE.test(value)) throw new TypeError(`BigQuery ${label} is invalid`);
|
|
32
|
+
}
|
|
33
|
+
const loadTimeout = options.loadTimeoutMilliseconds ?? 6e5;
|
|
34
|
+
if (!Number.isSafeInteger(loadTimeout) || loadTimeout < 1e3 || loadTimeout > 6 * 36e5) {
|
|
35
|
+
throw new RangeError("loadTimeoutMilliseconds must be between 1 second and 6 hours");
|
|
36
|
+
}
|
|
37
|
+
const maxRevisions = options.maxRevisionsPerBatch ?? 200;
|
|
38
|
+
if (!Number.isSafeInteger(maxRevisions) || maxRevisions < 1 || maxRevisions > 1e4) {
|
|
39
|
+
throw new RangeError("maxRevisionsPerBatch must be between 1 and 10000");
|
|
40
|
+
}
|
|
41
|
+
const tables = {
|
|
42
|
+
projectId: options.projectId,
|
|
43
|
+
datasetId: options.datasetId,
|
|
44
|
+
rowsTableId: options.rowsTableId,
|
|
45
|
+
revisionsTableId: options.revisionsTableId
|
|
46
|
+
};
|
|
47
|
+
const dataset = options.bigquery.dataset(options.datasetId, {
|
|
48
|
+
...options.projectId ? { projectId: options.projectId } : {},
|
|
49
|
+
...options.location ? { location: options.location } : {}
|
|
50
|
+
});
|
|
51
|
+
async function loadOnce(tableId, jobId, lines, signal) {
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
const directory = await fs.mkdtemp(path.join(tmpdir(), "adcp-warehouse-"));
|
|
54
|
+
const file = path.join(directory, `${jobId}.ndjson`);
|
|
55
|
+
try {
|
|
56
|
+
await fs.writeFile(file, lines.length ? lines.join("\n") + "\n" : "");
|
|
57
|
+
try {
|
|
58
|
+
await withinDeadline(
|
|
59
|
+
() => dataset.table(tableId).load(file, {
|
|
60
|
+
jobId,
|
|
61
|
+
sourceFormat: "NEWLINE_DELIMITED_JSON",
|
|
62
|
+
writeDisposition: "WRITE_APPEND",
|
|
63
|
+
jobTimeoutMs: String(loadTimeout),
|
|
64
|
+
...options.location ? { location: options.location } : {},
|
|
65
|
+
...options.labels ? { labels: { ...options.labels } } : {}
|
|
66
|
+
}),
|
|
67
|
+
loadTimeout,
|
|
68
|
+
signal
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isAlreadyExists(error)) throw error;
|
|
72
|
+
const metadata = await withinDeadline(
|
|
73
|
+
() => options.bigquery.job(jobId, options.location ? { location: options.location } : void 0).getMetadata(),
|
|
74
|
+
loadTimeout,
|
|
75
|
+
signal
|
|
76
|
+
);
|
|
77
|
+
assertJobSucceeded(metadata);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
defaultTablesSql: reportingWarehouseDefaultTablesSqlV1(tables),
|
|
85
|
+
currentRowsViewSql: reportingWarehouseCurrentRowsViewSqlV1(tables),
|
|
86
|
+
async runOnce(runOptions = {}) {
|
|
87
|
+
const signal = runOptions.signal;
|
|
88
|
+
await options.db.query(
|
|
89
|
+
`INSERT INTO adcp_reporting_warehouse_sinks (sink_name) VALUES ($1) ON CONFLICT DO NOTHING`,
|
|
90
|
+
[options.name]
|
|
91
|
+
);
|
|
92
|
+
const state = (await options.db.query(
|
|
93
|
+
`SELECT committed_cursor, planned_from_cursor, planned_to_cursor, planned_revisions,
|
|
94
|
+
planned_rows_attempt, planned_revisions_attempt
|
|
95
|
+
FROM adcp_reporting_warehouse_sinks WHERE sink_name = $1`,
|
|
96
|
+
[options.name]
|
|
97
|
+
)).rows[0];
|
|
98
|
+
let plan = state.planned_to_cursor && state.planned_revisions ? {
|
|
99
|
+
from: state.planned_from_cursor,
|
|
100
|
+
to: state.planned_to_cursor,
|
|
101
|
+
revisions: state.planned_revisions,
|
|
102
|
+
rowsAttempt: state.planned_rows_attempt,
|
|
103
|
+
revisionsAttempt: state.planned_revisions_attempt
|
|
104
|
+
} : void 0;
|
|
105
|
+
if (!plan) {
|
|
106
|
+
const page = await options.store.changesAfter({
|
|
107
|
+
...state.committed_cursor ? { cursor: state.committed_cursor } : {},
|
|
108
|
+
...options.account_id ? { account_id: options.account_id } : {},
|
|
109
|
+
kinds: ["revision"],
|
|
110
|
+
limit: maxRevisions
|
|
111
|
+
});
|
|
112
|
+
if (page.records.length === 0) {
|
|
113
|
+
if (page.cursor !== state.committed_cursor) await commit(page.cursor);
|
|
114
|
+
return { idle: true, revisions: 0, rows: 0, missing: [] };
|
|
115
|
+
}
|
|
116
|
+
plan = {
|
|
117
|
+
from: state.committed_cursor,
|
|
118
|
+
to: page.cursor,
|
|
119
|
+
revisions: page.records.map((record) => ({ id: record.record_id, account_id: record.account_id })),
|
|
120
|
+
rowsAttempt: 0,
|
|
121
|
+
revisionsAttempt: 0
|
|
122
|
+
};
|
|
123
|
+
await options.db.query(
|
|
124
|
+
`UPDATE adcp_reporting_warehouse_sinks
|
|
125
|
+
SET planned_from_cursor = $2, planned_to_cursor = $3, planned_revisions = $4::jsonb,
|
|
126
|
+
planned_rows_attempt = 0, planned_revisions_attempt = 0, updated_at = clock_timestamp()
|
|
127
|
+
WHERE sink_name = $1`,
|
|
128
|
+
[options.name, plan.from, plan.to, JSON.stringify(plan.revisions)]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const rowLines = [];
|
|
132
|
+
const revisionLines = [];
|
|
133
|
+
const missing = [];
|
|
134
|
+
for (const planned of plan.revisions) {
|
|
135
|
+
signal?.throwIfAborted();
|
|
136
|
+
const revision = await options.store.getRevisionMetadata(planned.id, planned.account_id);
|
|
137
|
+
if (!revision) {
|
|
138
|
+
missing.push(planned.id);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const periodDate = sourceLocalDate(
|
|
142
|
+
revision.wireRevision.period?.start,
|
|
143
|
+
revision.wireRevision.period?.source_timezone
|
|
144
|
+
);
|
|
145
|
+
const base = {
|
|
146
|
+
reporting_revision_id: revision.reporting_revision_id,
|
|
147
|
+
reporting_obligation_id: revision.reporting_obligation_id,
|
|
148
|
+
account_id: planned.account_id,
|
|
149
|
+
revision_number: revision.revisionNumber,
|
|
150
|
+
finality: revision.finality,
|
|
151
|
+
period_date: periodDate
|
|
152
|
+
};
|
|
153
|
+
for (let offset = 0; offset < revision.binding.rowCount; ) {
|
|
154
|
+
const page = await options.store.readRevisionRows({
|
|
155
|
+
reporting_revision_id: revision.reporting_revision_id,
|
|
156
|
+
account_id: planned.account_id,
|
|
157
|
+
offset,
|
|
158
|
+
limit: 5e3,
|
|
159
|
+
...signal ? { signal } : {}
|
|
160
|
+
});
|
|
161
|
+
if (!page || page.rows.length === 0) throw new Error("Reporting revision rows ended early");
|
|
162
|
+
page.rows.forEach((row, index) => {
|
|
163
|
+
const ordinal = offset + index;
|
|
164
|
+
const mapped = options.mapRow ? options.mapRow(row, { revision, ordinal }) : { row };
|
|
165
|
+
rowLines.push(JSON.stringify({ ...mapped, ...base, ordinal }));
|
|
166
|
+
});
|
|
167
|
+
offset += page.rows.length;
|
|
168
|
+
}
|
|
169
|
+
revisionLines.push(
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
...base,
|
|
172
|
+
supersedes_reporting_revision_id: revision.supersedes_reporting_revision_id ?? null,
|
|
173
|
+
period_start: revision.wireRevision.period?.start ?? null,
|
|
174
|
+
period_end: revision.wireRevision.period?.end ?? null,
|
|
175
|
+
source_timezone: revision.wireRevision.period?.source_timezone ?? null,
|
|
176
|
+
observed_at: revision.observedAt,
|
|
177
|
+
data_through: revision.dataThrough,
|
|
178
|
+
revision_content_sha256: revision.binding.sha256,
|
|
179
|
+
row_count: revision.binding.rowCount,
|
|
180
|
+
control_totals: revision.wireRevision.control_totals ?? []
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const baseJobId = reportingWarehouseJobIdV1(options.name, plan.from, plan.to);
|
|
185
|
+
const suffix = (attempt) => attempt ? `_a${attempt}` : "";
|
|
186
|
+
const steps = [
|
|
187
|
+
{ table: options.rowsTableId, kind: "rows", lines: rowLines, attempt: plan.rowsAttempt },
|
|
188
|
+
{ table: options.revisionsTableId, kind: "revs", lines: revisionLines, attempt: plan.revisionsAttempt }
|
|
189
|
+
];
|
|
190
|
+
for (const step of steps) {
|
|
191
|
+
if (!step.lines.length) continue;
|
|
192
|
+
try {
|
|
193
|
+
await loadOnce(step.table, `${baseJobId}_${step.kind}${suffix(step.attempt)}`, [...step.lines], signal);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (isFinishedWithError(error)) {
|
|
196
|
+
const column = step.kind === "rows" ? "planned_rows_attempt" : "planned_revisions_attempt";
|
|
197
|
+
await options.db.query(
|
|
198
|
+
`UPDATE adcp_reporting_warehouse_sinks SET ${column} = ${column} + 1,
|
|
199
|
+
updated_at = clock_timestamp() WHERE sink_name = $1`,
|
|
200
|
+
[options.name]
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await commit(plan.to);
|
|
207
|
+
return { idle: false, revisions: revisionLines.length, rows: rowLines.length, missing };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
async function commit(cursor) {
|
|
211
|
+
await options.db.query(
|
|
212
|
+
`UPDATE adcp_reporting_warehouse_sinks
|
|
213
|
+
SET committed_cursor = $2, planned_from_cursor = NULL, planned_to_cursor = NULL,
|
|
214
|
+
planned_revisions = NULL, planned_rows_attempt = 0, planned_revisions_attempt = 0,
|
|
215
|
+
updated_at = clock_timestamp()
|
|
216
|
+
WHERE sink_name = $1`,
|
|
217
|
+
[options.name, cursor]
|
|
218
|
+
);
|
|
219
|
+
await options.store.saveFeedConsumerCursor(options.name, cursor);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function reportingWarehouseJobIdV1(name, from, to) {
|
|
223
|
+
return `adcp_rr_${createHash("sha256").update(JSON.stringify([name, from, to])).digest("hex").slice(0, 40)}`;
|
|
224
|
+
}
|
|
225
|
+
function qualified(tables, table) {
|
|
226
|
+
return `\`${tables.projectId ? `${tables.projectId}.` : ""}${tables.datasetId}.${table}\``;
|
|
227
|
+
}
|
|
228
|
+
function reportingWarehouseDefaultTablesSqlV1(tables) {
|
|
229
|
+
return `
|
|
230
|
+
CREATE TABLE IF NOT EXISTS ${qualified(tables, tables.rowsTableId)} (
|
|
231
|
+
reporting_revision_id STRING NOT NULL,
|
|
232
|
+
reporting_obligation_id STRING NOT NULL,
|
|
233
|
+
account_id STRING NOT NULL,
|
|
234
|
+
revision_number INT64 NOT NULL,
|
|
235
|
+
finality STRING NOT NULL,
|
|
236
|
+
period_date DATE,
|
|
237
|
+
ordinal INT64 NOT NULL,
|
|
238
|
+
row JSON
|
|
239
|
+
)
|
|
240
|
+
PARTITION BY period_date
|
|
241
|
+
CLUSTER BY account_id, reporting_revision_id;
|
|
242
|
+
|
|
243
|
+
CREATE TABLE IF NOT EXISTS ${qualified(tables, tables.revisionsTableId)} (
|
|
244
|
+
reporting_revision_id STRING NOT NULL,
|
|
245
|
+
reporting_obligation_id STRING NOT NULL,
|
|
246
|
+
account_id STRING NOT NULL,
|
|
247
|
+
revision_number INT64 NOT NULL,
|
|
248
|
+
finality STRING NOT NULL,
|
|
249
|
+
period_date DATE,
|
|
250
|
+
supersedes_reporting_revision_id STRING,
|
|
251
|
+
period_start TIMESTAMP,
|
|
252
|
+
period_end TIMESTAMP,
|
|
253
|
+
source_timezone STRING,
|
|
254
|
+
observed_at TIMESTAMP,
|
|
255
|
+
data_through TIMESTAMP,
|
|
256
|
+
revision_content_sha256 STRING NOT NULL,
|
|
257
|
+
row_count INT64 NOT NULL,
|
|
258
|
+
control_totals JSON
|
|
259
|
+
)
|
|
260
|
+
PARTITION BY period_date
|
|
261
|
+
CLUSTER BY account_id, reporting_obligation_id;`.trim();
|
|
262
|
+
}
|
|
263
|
+
function reportingWarehouseCurrentRowsViewSqlV1(tables) {
|
|
264
|
+
const view = tables.viewId ?? "current_rows";
|
|
265
|
+
if (!DATASET_OR_TABLE.test(view)) throw new TypeError("BigQuery viewId is invalid");
|
|
266
|
+
return `
|
|
267
|
+
CREATE OR REPLACE VIEW ${qualified(tables, view)} AS
|
|
268
|
+
SELECT row_data.*
|
|
269
|
+
FROM ${qualified(tables, tables.rowsTableId)} AS row_data
|
|
270
|
+
JOIN (
|
|
271
|
+
SELECT reporting_obligation_id,
|
|
272
|
+
ARRAY_AGG(reporting_revision_id ORDER BY revision_number DESC LIMIT 1)[OFFSET(0)] AS reporting_revision_id
|
|
273
|
+
FROM ${qualified(tables, tables.revisionsTableId)}
|
|
274
|
+
GROUP BY reporting_obligation_id
|
|
275
|
+
) AS current_revision
|
|
276
|
+
USING (reporting_obligation_id, reporting_revision_id);`.trim();
|
|
277
|
+
}
|
|
278
|
+
function sourceLocalDate(start, timeZone) {
|
|
279
|
+
if (!start) return null;
|
|
280
|
+
const instant = new Date(start);
|
|
281
|
+
if (Number.isNaN(instant.getTime())) return null;
|
|
282
|
+
try {
|
|
283
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
284
|
+
timeZone: timeZone ?? "UTC",
|
|
285
|
+
year: "numeric",
|
|
286
|
+
month: "2-digit",
|
|
287
|
+
day: "2-digit"
|
|
288
|
+
}).format(instant);
|
|
289
|
+
return parts;
|
|
290
|
+
} catch {
|
|
291
|
+
return instant.toISOString().slice(0, 10);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function withinDeadline(operation, milliseconds, signal) {
|
|
295
|
+
signal?.throwIfAborted();
|
|
296
|
+
let timer;
|
|
297
|
+
let onAbort;
|
|
298
|
+
try {
|
|
299
|
+
return await Promise.race([
|
|
300
|
+
operation(),
|
|
301
|
+
new Promise((_, reject) => {
|
|
302
|
+
timer = setTimeout(
|
|
303
|
+
() => reject(new Error("BigQuery call exceeded its deadline; the batch will be retried")),
|
|
304
|
+
milliseconds
|
|
305
|
+
);
|
|
306
|
+
if (signal) {
|
|
307
|
+
onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
308
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
]);
|
|
312
|
+
} finally {
|
|
313
|
+
if (timer) clearTimeout(timer);
|
|
314
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function isAlreadyExists(error) {
|
|
318
|
+
const record = error;
|
|
319
|
+
return record?.code === 409 || !!record?.errors?.some((entry) => entry.reason === "duplicate");
|
|
320
|
+
}
|
|
321
|
+
function isFinishedWithError(error) {
|
|
322
|
+
return error?.adcpJobFailed === true;
|
|
323
|
+
}
|
|
324
|
+
function assertJobSucceeded(metadata) {
|
|
325
|
+
const record = Array.isArray(metadata) ? metadata[0] : metadata;
|
|
326
|
+
if (record?.status?.state !== "DONE") {
|
|
327
|
+
throw new Error("A BigQuery load job for this batch is still running; retry later");
|
|
328
|
+
}
|
|
329
|
+
if (record.status.errorResult) {
|
|
330
|
+
const failure = new Error("A BigQuery load job for this batch failed; it will be retried as a new attempt");
|
|
331
|
+
Object.defineProperty(failure, "adcpJobFailed", { value: true });
|
|
332
|
+
throw failure;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
REPORTING_WAREHOUSE_SINK_MIGRATION,
|
|
337
|
+
createBigQueryReportingWarehouseSinkV1,
|
|
338
|
+
reportingWarehouseCurrentRowsViewSqlV1,
|
|
339
|
+
reportingWarehouseDefaultTablesSqlV1,
|
|
340
|
+
reportingWarehouseJobIdV1
|
|
341
|
+
};
|
|
@@ -410,6 +410,12 @@ function createReportingStatusHandler(store, options) {
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
const REPORTING_STATUS_REVISION_VIEW_MAX_ROWS = 1e4;
|
|
413
|
+
function reportingRevisionNotFound() {
|
|
414
|
+
return new import_async_outcome.AdcpError("REFERENCE_NOT_FOUND", {
|
|
415
|
+
message: "Reporting revision is unavailable",
|
|
416
|
+
field: "reporting_revision_id"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
413
419
|
function createReportingDeliveryHandler(store) {
|
|
414
420
|
const activeReadsByAccount = /* @__PURE__ */ new Map();
|
|
415
421
|
return async (request, context) => {
|
|
@@ -428,14 +434,15 @@ function createReportingDeliveryHandler(store) {
|
|
|
428
434
|
try {
|
|
429
435
|
const raw = request;
|
|
430
436
|
if (typeof raw.reporting_revision_id !== "string" || !raw.reporting_revision_id) {
|
|
431
|
-
throw new
|
|
437
|
+
throw new import_async_outcome.AdcpError("VALIDATION_ERROR", {
|
|
438
|
+
message: "Ledger delivery reads require reporting_revision_id",
|
|
439
|
+
field: "reporting_revision_id"
|
|
440
|
+
});
|
|
432
441
|
}
|
|
433
442
|
const revision = await (0, import_revision_metadata.getRevisionMetadataFromStore)(store, raw.reporting_revision_id, accountId);
|
|
434
|
-
if (!revision) throw
|
|
443
|
+
if (!revision) throw reportingRevisionNotFound();
|
|
435
444
|
const obligation = await store.getObligation(revision.reporting_obligation_id);
|
|
436
|
-
if (!obligation || obligation.account.account_id !== accountId)
|
|
437
|
-
throw new Error("Reporting revision is unavailable");
|
|
438
|
-
}
|
|
445
|
+
if (!obligation || obligation.account.account_id !== accountId) throw reportingRevisionNotFound();
|
|
439
446
|
const pagination = isRecord(raw.pagination) ? raw.pagination : {};
|
|
440
447
|
const maxResults = typeof pagination.max_results === "number" && Number.isFinite(pagination.max_results) ? Math.max(1, Math.min(500, Math.trunc(pagination.max_results))) : 100;
|
|
441
448
|
const totalCount = revision.binding.rowCount;
|
|
@@ -448,7 +455,7 @@ function createReportingDeliveryHandler(store) {
|
|
|
448
455
|
}
|
|
449
456
|
offset = cursor.offset;
|
|
450
457
|
} catch {
|
|
451
|
-
throw
|
|
458
|
+
throw reportingRevisionNotFound();
|
|
452
459
|
}
|
|
453
460
|
}
|
|
454
461
|
let reportingRows = [];
|
|
@@ -464,14 +471,19 @@ function createReportingDeliveryHandler(store) {
|
|
|
464
471
|
});
|
|
465
472
|
} catch (error) {
|
|
466
473
|
if ((0, import_row_storage.isReportingRowStoreError)(error)) {
|
|
467
|
-
if (error.code === "ROWS_EXPIRED") throw
|
|
474
|
+
if (error.code === "ROWS_EXPIRED") throw reportingRevisionNotFound();
|
|
468
475
|
throw new import_async_outcome.AdcpError("SERVICE_UNAVAILABLE", {
|
|
469
476
|
message: "Reporting revision rows are temporarily unavailable"
|
|
470
477
|
});
|
|
471
478
|
}
|
|
472
479
|
throw error;
|
|
473
480
|
}
|
|
474
|
-
if (!page
|
|
481
|
+
if (!page) throw reportingRevisionNotFound();
|
|
482
|
+
if (page.total !== totalCount) {
|
|
483
|
+
throw new import_async_outcome.AdcpError("SERVICE_UNAVAILABLE", {
|
|
484
|
+
message: "Reporting revision rows are temporarily unavailable"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
475
487
|
reportingRows = page.rows;
|
|
476
488
|
}
|
|
477
489
|
const nextOffset = offset + reportingRows.length;
|
|
@@ -393,6 +393,12 @@ function createReportingStatusHandler(store, options) {
|
|
|
393
393
|
};
|
|
394
394
|
}
|
|
395
395
|
const REPORTING_STATUS_REVISION_VIEW_MAX_ROWS = 1e4;
|
|
396
|
+
function reportingRevisionNotFound() {
|
|
397
|
+
return new AdcpError("REFERENCE_NOT_FOUND", {
|
|
398
|
+
message: "Reporting revision is unavailable",
|
|
399
|
+
field: "reporting_revision_id"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
396
402
|
function createReportingDeliveryHandler(store) {
|
|
397
403
|
const activeReadsByAccount = /* @__PURE__ */ new Map();
|
|
398
404
|
return async (request, context) => {
|
|
@@ -411,14 +417,15 @@ function createReportingDeliveryHandler(store) {
|
|
|
411
417
|
try {
|
|
412
418
|
const raw = request;
|
|
413
419
|
if (typeof raw.reporting_revision_id !== "string" || !raw.reporting_revision_id) {
|
|
414
|
-
throw new
|
|
420
|
+
throw new AdcpError("VALIDATION_ERROR", {
|
|
421
|
+
message: "Ledger delivery reads require reporting_revision_id",
|
|
422
|
+
field: "reporting_revision_id"
|
|
423
|
+
});
|
|
415
424
|
}
|
|
416
425
|
const revision = await getRevisionMetadataFromStore(store, raw.reporting_revision_id, accountId);
|
|
417
|
-
if (!revision) throw
|
|
426
|
+
if (!revision) throw reportingRevisionNotFound();
|
|
418
427
|
const obligation = await store.getObligation(revision.reporting_obligation_id);
|
|
419
|
-
if (!obligation || obligation.account.account_id !== accountId)
|
|
420
|
-
throw new Error("Reporting revision is unavailable");
|
|
421
|
-
}
|
|
428
|
+
if (!obligation || obligation.account.account_id !== accountId) throw reportingRevisionNotFound();
|
|
422
429
|
const pagination = isRecord(raw.pagination) ? raw.pagination : {};
|
|
423
430
|
const maxResults = typeof pagination.max_results === "number" && Number.isFinite(pagination.max_results) ? Math.max(1, Math.min(500, Math.trunc(pagination.max_results))) : 100;
|
|
424
431
|
const totalCount = revision.binding.rowCount;
|
|
@@ -431,7 +438,7 @@ function createReportingDeliveryHandler(store) {
|
|
|
431
438
|
}
|
|
432
439
|
offset = cursor.offset;
|
|
433
440
|
} catch {
|
|
434
|
-
throw
|
|
441
|
+
throw reportingRevisionNotFound();
|
|
435
442
|
}
|
|
436
443
|
}
|
|
437
444
|
let reportingRows = [];
|
|
@@ -447,14 +454,19 @@ function createReportingDeliveryHandler(store) {
|
|
|
447
454
|
});
|
|
448
455
|
} catch (error) {
|
|
449
456
|
if (isReportingRowStoreError(error)) {
|
|
450
|
-
if (error.code === "ROWS_EXPIRED") throw
|
|
457
|
+
if (error.code === "ROWS_EXPIRED") throw reportingRevisionNotFound();
|
|
451
458
|
throw new AdcpError("SERVICE_UNAVAILABLE", {
|
|
452
459
|
message: "Reporting revision rows are temporarily unavailable"
|
|
453
460
|
});
|
|
454
461
|
}
|
|
455
462
|
throw error;
|
|
456
463
|
}
|
|
457
|
-
if (!page
|
|
464
|
+
if (!page) throw reportingRevisionNotFound();
|
|
465
|
+
if (page.total !== totalCount) {
|
|
466
|
+
throw new AdcpError("SERVICE_UNAVAILABLE", {
|
|
467
|
+
message: "Reporting revision rows are temporarily unavailable"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
458
470
|
reportingRows = page.rows;
|
|
459
471
|
}
|
|
460
472
|
const nextOffset = offset + reportingRows.length;
|
|
@@ -137,6 +137,17 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
|
|
|
137
137
|
private readonly rowStorageWrites;
|
|
138
138
|
private readonly changeFeed;
|
|
139
139
|
constructor(pool: ReportingPgPool, options?: PostgresReportingLedgerStoreOptions);
|
|
140
|
+
/** Whether this store writes the host change feed (`changeFeed: true`). */
|
|
141
|
+
get changeFeedEnabled(): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Delete expired cursor snapshots and changes checkpoints, a bounded batch
|
|
144
|
+
* at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
|
|
145
|
+
* hosts that hold the store but not the pool.
|
|
146
|
+
*/
|
|
147
|
+
sweepExpiredState(limit?: number): Promise<{
|
|
148
|
+
snapshotsDeleted: number;
|
|
149
|
+
checkpointsDeleted: number;
|
|
150
|
+
}>;
|
|
140
151
|
/** Append one host change-feed row inside the caller's account-locked transaction. */
|
|
141
152
|
private appendChange;
|
|
142
153
|
/** Probe and register row storage when enabled. Safe to call repeatedly. */
|
|
@@ -280,10 +291,20 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
|
|
|
280
291
|
*
|
|
281
292
|
* Planners skip retired periods, and `ledger_retained_from` advances past
|
|
282
293
|
* them because their obligations no longer exist.
|
|
294
|
+
*
|
|
295
|
+
* When the change-feed tables exist, a period that is not yet retiring is
|
|
296
|
+
* also held while any live registered feed consumer (updated within
|
|
297
|
+
* `maxFeedHoldDays`, default 7) has not passed that period's change rows in
|
|
298
|
+
* the consumer's own feed order, the same rule `pruneChanges` applies. A
|
|
299
|
+
* held period is simply not a candidate this pass. A consumer that stays
|
|
300
|
+
* silent past `maxFeedHoldDays` stops holding, so retention is never
|
|
301
|
+
* blocked forever; it then falls behind retention and a sink sees the
|
|
302
|
+
* retired revisions as missing.
|
|
283
303
|
*/
|
|
284
304
|
retireExpiredPeriods(input: {
|
|
285
305
|
statusRetentionDays: number;
|
|
286
306
|
recordRetentionDays?: number;
|
|
307
|
+
maxFeedHoldDays?: number;
|
|
287
308
|
limit?: number;
|
|
288
309
|
account_id?: string;
|
|
289
310
|
signal?: AbortSignal;
|
|
@@ -137,6 +137,17 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
|
|
|
137
137
|
private readonly rowStorageWrites;
|
|
138
138
|
private readonly changeFeed;
|
|
139
139
|
constructor(pool: ReportingPgPool, options?: PostgresReportingLedgerStoreOptions);
|
|
140
|
+
/** Whether this store writes the host change feed (`changeFeed: true`). */
|
|
141
|
+
get changeFeedEnabled(): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Delete expired cursor snapshots and changes checkpoints, a bounded batch
|
|
144
|
+
* at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
|
|
145
|
+
* hosts that hold the store but not the pool.
|
|
146
|
+
*/
|
|
147
|
+
sweepExpiredState(limit?: number): Promise<{
|
|
148
|
+
snapshotsDeleted: number;
|
|
149
|
+
checkpointsDeleted: number;
|
|
150
|
+
}>;
|
|
140
151
|
/** Append one host change-feed row inside the caller's account-locked transaction. */
|
|
141
152
|
private appendChange;
|
|
142
153
|
/** Probe and register row storage when enabled. Safe to call repeatedly. */
|
|
@@ -280,10 +291,20 @@ export declare class PostgresReportingLedgerStore implements ReportingLedgerStor
|
|
|
280
291
|
*
|
|
281
292
|
* Planners skip retired periods, and `ledger_retained_from` advances past
|
|
282
293
|
* them because their obligations no longer exist.
|
|
294
|
+
*
|
|
295
|
+
* When the change-feed tables exist, a period that is not yet retiring is
|
|
296
|
+
* also held while any live registered feed consumer (updated within
|
|
297
|
+
* `maxFeedHoldDays`, default 7) has not passed that period's change rows in
|
|
298
|
+
* the consumer's own feed order, the same rule `pruneChanges` applies. A
|
|
299
|
+
* held period is simply not a candidate this pass. A consumer that stays
|
|
300
|
+
* silent past `maxFeedHoldDays` stops holding, so retention is never
|
|
301
|
+
* blocked forever; it then falls behind retention and a sink sees the
|
|
302
|
+
* retired revisions as missing.
|
|
283
303
|
*/
|
|
284
304
|
retireExpiredPeriods(input: {
|
|
285
305
|
statusRetentionDays: number;
|
|
286
306
|
recordRetentionDays?: number;
|
|
307
|
+
maxFeedHoldDays?: number;
|
|
287
308
|
limit?: number;
|
|
288
309
|
account_id?: string;
|
|
289
310
|
signal?: AbortSignal;
|
|
@@ -424,6 +424,18 @@ class PostgresReportingLedgerStore {
|
|
|
424
424
|
rowStorage;
|
|
425
425
|
rowStorageWrites;
|
|
426
426
|
changeFeed;
|
|
427
|
+
/** Whether this store writes the host change feed (`changeFeed: true`). */
|
|
428
|
+
get changeFeedEnabled() {
|
|
429
|
+
return this.changeFeed;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Delete expired cursor snapshots and changes checkpoints, a bounded batch
|
|
433
|
+
* at a time. The store-bound form of `sweepExpiredReportingLedgerState`, for
|
|
434
|
+
* hosts that hold the store but not the pool.
|
|
435
|
+
*/
|
|
436
|
+
async sweepExpiredState(limit = 1e3) {
|
|
437
|
+
return sweepExpiredReportingLedgerState(this.pool, limit);
|
|
438
|
+
}
|
|
427
439
|
/** Append one host change-feed row inside the caller's account-locked transaction. */
|
|
428
440
|
async appendChange(client, kind, accountId, recordId, obligationId) {
|
|
429
441
|
if (!this.changeFeed) return;
|
|
@@ -1289,6 +1301,15 @@ ${managedDueArm} )
|
|
|
1289
1301
|
*
|
|
1290
1302
|
* Planners skip retired periods, and `ledger_retained_from` advances past
|
|
1291
1303
|
* them because their obligations no longer exist.
|
|
1304
|
+
*
|
|
1305
|
+
* When the change-feed tables exist, a period that is not yet retiring is
|
|
1306
|
+
* also held while any live registered feed consumer (updated within
|
|
1307
|
+
* `maxFeedHoldDays`, default 7) has not passed that period's change rows in
|
|
1308
|
+
* the consumer's own feed order, the same rule `pruneChanges` applies. A
|
|
1309
|
+
* held period is simply not a candidate this pass. A consumer that stays
|
|
1310
|
+
* silent past `maxFeedHoldDays` stops holding, so retention is never
|
|
1311
|
+
* blocked forever; it then falls behind retention and a sink sees the
|
|
1312
|
+
* retired revisions as missing.
|
|
1292
1313
|
*/
|
|
1293
1314
|
async retireExpiredPeriods(input) {
|
|
1294
1315
|
if (!Number.isSafeInteger(input.statusRetentionDays) || input.statusRetentionDays < 1) {
|
|
@@ -1300,8 +1321,18 @@ ${managedDueArm} )
|
|
|
1300
1321
|
}
|
|
1301
1322
|
const limit = input.limit ?? 100;
|
|
1302
1323
|
positiveInteger(limit, "limit");
|
|
1324
|
+
const holdDays = input.maxFeedHoldDays ?? 7;
|
|
1325
|
+
positiveInteger(holdDays, "maxFeedHoldDays");
|
|
1303
1326
|
const tables = await this.optionalTables();
|
|
1304
1327
|
const materializationHold = tables.has("adcp_reporting_materializations") ? `AND NOT EXISTS (SELECT 1 FROM adcp_reporting_materializations hold WHERE hold.obligation_id = obligation.obligation_id)` : "";
|
|
1328
|
+
const feedHold = tables.has("adcp_reporting_changes") && tables.has("adcp_reporting_feed_consumers") ? `AND NOT EXISTS (
|
|
1329
|
+
SELECT 1 FROM adcp_reporting_changes change
|
|
1330
|
+
JOIN adcp_reporting_feed_consumers consumer
|
|
1331
|
+
ON consumer.updated_at > clock_timestamp() - ($4::integer * INTERVAL '1 day')
|
|
1332
|
+
WHERE change.obligation_id = obligation.obligation_id
|
|
1333
|
+
AND ((consumer.account_id IS NULL
|
|
1334
|
+
AND (change.xid, change.seq) > (consumer.cursor_xid, consumer.cursor_seq))
|
|
1335
|
+
OR (consumer.account_id = change.account_id AND change.seq > consumer.cursor_seq)))` : "";
|
|
1305
1336
|
const candidates = await this.query(
|
|
1306
1337
|
`SELECT obligation_id, account_id FROM (
|
|
1307
1338
|
SELECT tombstone.obligation_id, tombstone.account_id, 0 AS resume, tombstone.period_end
|
|
@@ -1322,10 +1353,11 @@ ${managedDueArm} )
|
|
|
1322
1353
|
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1323
1354
|
WHERE tombstone.obligation_id = obligation.obligation_id)
|
|
1324
1355
|
${materializationHold}
|
|
1356
|
+
${feedHold}
|
|
1325
1357
|
) candidate
|
|
1326
1358
|
ORDER BY resume, period_end, obligation_id
|
|
1327
1359
|
LIMIT $1`,
|
|
1328
|
-
[limit, input.account_id ?? null, recordDays]
|
|
1360
|
+
feedHold ? [limit, input.account_id ?? null, recordDays, holdDays] : [limit, input.account_id ?? null, recordDays]
|
|
1329
1361
|
);
|
|
1330
1362
|
let retired = 0;
|
|
1331
1363
|
const failed = [];
|
|
@@ -1497,7 +1529,8 @@ ${managedDueArm} )
|
|
|
1497
1529
|
`SELECT table_name AS name FROM information_schema.tables
|
|
1498
1530
|
WHERE table_schema = current_schema()
|
|
1499
1531
|
AND table_name IN ('adcp_reporting_row_sets', 'adcp_reporting_row_write_intents',
|
|
1500
|
-
'adcp_reporting_materializations'
|
|
1532
|
+
'adcp_reporting_materializations', 'adcp_reporting_changes',
|
|
1533
|
+
'adcp_reporting_feed_consumers')`
|
|
1501
1534
|
);
|
|
1502
1535
|
return new Set(result.rows.map((row) => row.name));
|
|
1503
1536
|
}
|