@oxygen-agent/cli 1.1014.985 → 1.1014.1087
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 +1 -1
- package/dist/command-families/program.d.ts +2 -0
- package/dist/command-families/program.js +241 -0
- package/dist/command-families/tables/automation.d.ts +22 -0
- package/dist/command-families/tables/automation.js +250 -0
- package/dist/command-families/tables/bundle-export.d.ts +7 -0
- package/dist/command-families/tables/bundle-export.js +198 -0
- package/dist/command-families/tables/bundle-format.d.ts +66 -0
- package/dist/command-families/tables/bundle-format.js +274 -0
- package/dist/command-families/tables/bundle-import.d.ts +20 -0
- package/dist/command-families/tables/bundle-import.js +260 -0
- package/dist/command-families/tables/display.d.ts +107 -0
- package/dist/command-families/tables/display.js +229 -0
- package/dist/command-families/tables/import-batches.d.ts +26 -0
- package/dist/command-families/tables/import-batches.js +94 -0
- package/dist/command-families/tables/import-columns.d.ts +24 -0
- package/dist/command-families/tables/import-columns.js +131 -0
- package/dist/command-families/tables/import-upload.d.ts +30 -0
- package/dist/command-families/tables/import-upload.js +203 -0
- package/dist/command-families/tables/import-values.d.ts +10 -0
- package/dist/command-families/tables/import-values.js +60 -0
- package/dist/command-families/tables/imports.d.ts +19 -0
- package/dist/command-families/tables/imports.js +200 -0
- package/dist/command-families/tables/options.d.ts +314 -0
- package/dist/command-families/tables/options.js +1 -0
- package/dist/command-families/tables/register-automation.d.ts +2 -0
- package/dist/command-families/tables/register-automation.js +127 -0
- package/dist/command-families/tables/register-external-imports.d.ts +2 -0
- package/dist/command-families/tables/register-external-imports.js +293 -0
- package/dist/command-families/tables/register-files.d.ts +2 -0
- package/dist/command-families/tables/register-files.js +72 -0
- package/dist/command-families/tables/register-followers.d.ts +2 -0
- package/dist/command-families/tables/register-followers.js +73 -0
- package/dist/command-families/tables/register-lifecycle.d.ts +2 -0
- package/dist/command-families/tables/register-lifecycle.js +114 -0
- package/dist/command-families/tables/register-reads.d.ts +2 -0
- package/dist/command-families/tables/register-reads.js +187 -0
- package/dist/command-families/tables/register-relations.d.ts +2 -0
- package/dist/command-families/tables/register-relations.js +244 -0
- package/dist/command-families/tables/register-row-actions.d.ts +2 -0
- package/dist/command-families/tables/register-row-actions.js +53 -0
- package/dist/command-families/tables/register-row-writes.d.ts +2 -0
- package/dist/command-families/tables/register-row-writes.js +140 -0
- package/dist/command-families/tables/register-views.d.ts +2 -0
- package/dist/command-families/tables/register-views.js +83 -0
- package/dist/command-families/tables/register-watchers.d.ts +2 -0
- package/dist/command-families/tables/register-watchers.js +65 -0
- package/dist/command-families/tables/register-webhooks.d.ts +2 -0
- package/dist/command-families/tables/register-webhooks.js +126 -0
- package/dist/command-families/tables/registration-context.d.ts +8 -0
- package/dist/command-families/tables/registration-context.js +1 -0
- package/dist/command-families/tables/registration.d.ts +3 -0
- package/dist/command-families/tables/registration.js +30 -0
- package/dist/command-families/tables/row-selection.d.ts +5 -0
- package/dist/command-families/tables/row-selection.js +82 -0
- package/dist/command-families/tables/send.d.ts +6 -0
- package/dist/command-families/tables/send.js +111 -0
- package/dist/command-manifest.js +1 -0
- package/dist/commands/copilot.js +8 -0
- package/dist/commands/deliverability.js +4 -2
- package/dist/commands/sequences.js +30 -2
- package/dist/commands/tables.d.ts +1 -3
- package/dist/commands/tables.js +1 -3505
- package/dist/credentials.js +1 -1
- package/dist/customer-operation-commands.js +62 -3
- package/dist/help.js +1 -1
- package/dist/index.d.ts +1 -99
- package/dist/index.js +5 -337
- package/dist/knowledge-mirror.js +1 -1
- package/dist/skills.js +4 -2
- package/dist/update.js +2 -1
- package/dist/workspace-placement-commands.d.ts +3 -0
- package/dist/workspace-placement-commands.js +32 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +11 -3
- package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/agents-enabled.js +51 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +39 -2
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +17 -15
- package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/index.js +0 -6
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +6 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +13 -8
- package/node_modules/@oxygen/shared/dist/teammates.js +20 -11
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +78 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +149 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +101 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { readRecordString } from "../../cli-values.js";
|
|
2
|
+
import { readRecord } from "../../command-support.js";
|
|
3
|
+
import { requestOxygen } from "../../http-client.js";
|
|
4
|
+
import { readOption } from "../../util.js";
|
|
5
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { SAFE_IMPORT_WRITE_BATCH_SIZE, normalizeImportBatchSize, tableWebUrl } from "./import-values.js";
|
|
8
|
+
import { openTableBundle, readBundleBatches } from "./bundle-format.js";
|
|
9
|
+
import { writeImportBatchWithAutoSplit } from "./import-batches.js";
|
|
10
|
+
export async function importTableBundle(// skipcq: JS-R1005
|
|
11
|
+
options, binaryName) {
|
|
12
|
+
const path = options.file;
|
|
13
|
+
if (!path || !path.trim()) {
|
|
14
|
+
throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
|
|
15
|
+
}
|
|
16
|
+
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
17
|
+
const effectiveBatchSize = Math.min(batchSize, SAFE_IMPORT_WRITE_BATCH_SIZE);
|
|
18
|
+
const bundle = await openTableBundle(resolve(path));
|
|
19
|
+
// A native `relation` column is only half of a relation: the other half is a
|
|
20
|
+
// row in ox_tables.relation_definitions that ONLY `tables relate` writes. A
|
|
21
|
+
// bundle carries the column but not the definition, and its `target_table` is
|
|
22
|
+
// the SOURCE workspace's table id, which does not exist here. Importing it
|
|
23
|
+
// produced a column that pointed at a foreign workspace and that no surface
|
|
24
|
+
// could repair, rename, archive or delete — every column lifecycle command
|
|
25
|
+
// refuses a relation-shaped column, and the relation commands refuse a
|
|
26
|
+
// definition that was never written (OXY-4360). Skip them and say so;
|
|
27
|
+
// `tables relate` rebuilds a real relation on this side once both tables
|
|
28
|
+
// exist. Keyed on the NATIVE shape, not `kind === "relation"`: CRM
|
|
29
|
+
// relationship columns (`target_object`, no `storage`) are a different
|
|
30
|
+
// mechanism with their own lifecycle and must still import.
|
|
31
|
+
const isNativeRelationColumn = (column) => column.kind === "relation"
|
|
32
|
+
&& (column.definition?.storage === "tables"
|
|
33
|
+
|| typeof column.definition?.archived_relation_definition_id === "string");
|
|
34
|
+
const bundleColumns = bundle.columns.map(bundleColumnToCreateInput);
|
|
35
|
+
const skippedRelationColumns = bundleColumns
|
|
36
|
+
.filter(isNativeRelationColumn)
|
|
37
|
+
.map((column) => column.key ?? column.label);
|
|
38
|
+
const columns = bundleColumns.filter((column) => !isNativeRelationColumn(column));
|
|
39
|
+
if (bundleColumns.length === 0) {
|
|
40
|
+
throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
|
|
41
|
+
}
|
|
42
|
+
if (columns.length === 0) {
|
|
43
|
+
throw new OxygenError("invalid_bundle", "Bundle contains only native relation columns, which cannot be imported directly. Import the tables they point at, then run `tables relate`.", { details: { skipped_relation_columns: skippedRelationColumns }, exitCode: 1 });
|
|
44
|
+
}
|
|
45
|
+
const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
|
|
46
|
+
const into = readOption(options.into);
|
|
47
|
+
const upsertKey = readOption(options.key);
|
|
48
|
+
// Resuming/appending into an existing table without an upsert key would
|
|
49
|
+
// duplicate every already-imported row. Require --key so the resume matches
|
|
50
|
+
// rows instead of re-inserting them.
|
|
51
|
+
if (into && !upsertKey) {
|
|
52
|
+
throw new OxygenError("invalid_input", "--into requires --key so rows already imported are matched instead of duplicated. Pass --key <uniqueColumn>.", { details: { into }, exitCode: 1 });
|
|
53
|
+
}
|
|
54
|
+
if (upsertKey) {
|
|
55
|
+
if (upsertKey.startsWith("_")) {
|
|
56
|
+
throw new OxygenError("invalid_input", "--key must be a workspace column key, not an internal field.", { details: { key: upsertKey }, exitCode: 1 });
|
|
57
|
+
}
|
|
58
|
+
if (validColumnKeys.size > 0 && !validColumnKeys.has(upsertKey)) {
|
|
59
|
+
throw new OxygenError("invalid_input", `--key ${upsertKey} is not a column in this bundle.`, { details: { key: upsertKey, available_keys: [...validColumnKeys] }, exitCode: 1 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Resolve the target table: resume into an existing one (--into), or create a
|
|
63
|
+
// fresh one from the bundle schema.
|
|
64
|
+
let newTableId;
|
|
65
|
+
let newTableSlug;
|
|
66
|
+
let createdTable;
|
|
67
|
+
if (into) {
|
|
68
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table: into } });
|
|
69
|
+
const tableMeta = describe.table ?? null;
|
|
70
|
+
const resolvedId = tableMeta ? readRecordString(tableMeta, "id") : null;
|
|
71
|
+
if (!resolvedId) {
|
|
72
|
+
throw new OxygenError("table_not_found", `--into table ${into} was not found in this org.`, { details: { into }, exitCode: 1 });
|
|
73
|
+
}
|
|
74
|
+
newTableId = resolvedId;
|
|
75
|
+
newTableSlug = tableMeta ? readRecordString(tableMeta, "slug") ?? null : null;
|
|
76
|
+
createdTable = false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const tableName = readOption(options.name) ?? bundle.tableName;
|
|
80
|
+
if (!tableName) {
|
|
81
|
+
throw new OxygenError("invalid_bundle", "Bundle has no table.name and no --name override was provided.", { exitCode: 1 });
|
|
82
|
+
}
|
|
83
|
+
const createResponse = await requestOxygen("/api/cli/tables", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: {
|
|
86
|
+
name: tableName,
|
|
87
|
+
columns,
|
|
88
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const created = readRecord(createResponse, "table");
|
|
92
|
+
const createdId = readRecordString(createResponse, "table_id")
|
|
93
|
+
?? (created ? readRecordString(created, "id") : null);
|
|
94
|
+
if (!createdId) {
|
|
95
|
+
throw new OxygenError("invalid_response", "Table create response did not include an id; bundle import cannot proceed.", { exitCode: 1 });
|
|
96
|
+
}
|
|
97
|
+
newTableId = createdId;
|
|
98
|
+
newTableSlug = readRecordString(createResponse, "table_slug")
|
|
99
|
+
?? (created ? readRecordString(created, "slug") : null);
|
|
100
|
+
createdTable = true;
|
|
101
|
+
}
|
|
102
|
+
// In upsert mode every row must carry the key value; otherwise the upsert
|
|
103
|
+
// route rejects the whole batch midway. A single-document bundle has every
|
|
104
|
+
// row in memory, so it is checked before anything is written and never
|
|
105
|
+
// leaves a half-imported table behind for a predictable data problem. A
|
|
106
|
+
// streamed bundle is checked row by row as it is read; a miss stops the
|
|
107
|
+
// import with the row number, and --key makes the rerun idempotent.
|
|
108
|
+
if (upsertKey && bundle.preloadedRows) {
|
|
109
|
+
const missingIndex = bundle.preloadedRows.findIndex((row) => row[upsertKey] === undefined || row[upsertKey] === null);
|
|
110
|
+
if (missingIndex >= 0) {
|
|
111
|
+
throw new OxygenError("invalid_bundle", `Row ${missingIndex + 1} is missing a value for the upsert key "${upsertKey}".`, { details: { key: upsertKey, row_number: missingIndex + 1 }, exitCode: 1 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const resumeCommand = buildBundleResumeCommand({
|
|
115
|
+
file: path,
|
|
116
|
+
tableId: newTableId,
|
|
117
|
+
key: upsertKey ?? null,
|
|
118
|
+
batchSize: effectiveBatchSize,
|
|
119
|
+
binaryName,
|
|
120
|
+
});
|
|
121
|
+
let processed = 0;
|
|
122
|
+
let insertedCount = 0;
|
|
123
|
+
let updatedCount = 0;
|
|
124
|
+
const batches = readBundleBatches({
|
|
125
|
+
bundle,
|
|
126
|
+
validColumnKeys,
|
|
127
|
+
upsertKey: upsertKey ?? null,
|
|
128
|
+
batchSize: effectiveBatchSize,
|
|
129
|
+
tableId: newTableId,
|
|
130
|
+
resumeCommand,
|
|
131
|
+
});
|
|
132
|
+
for await (const batch of batches) {
|
|
133
|
+
try {
|
|
134
|
+
// Same byte-aware writer as `tables import`: a wide batch pre-splits (and a
|
|
135
|
+
// server 413 halves) instead of dead-ending the bundle at an unchanged
|
|
136
|
+
// resume batch size. No request_id — bundle resumes match on --key.
|
|
137
|
+
const result = await writeImportBatchWithAutoSplit({
|
|
138
|
+
tableRef: newTableId,
|
|
139
|
+
upsertKey: upsertKey ?? undefined,
|
|
140
|
+
rows: batch,
|
|
141
|
+
});
|
|
142
|
+
if (upsertKey) {
|
|
143
|
+
insertedCount += result.insertedCount;
|
|
144
|
+
updatedCount += result.updatedCount;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
insertedCount += batch.length;
|
|
148
|
+
}
|
|
149
|
+
processed += batch.length;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// A mid-loop failure used to leave a partial table with no way forward.
|
|
153
|
+
// Surface exactly where it stopped and how to resume so the import is
|
|
154
|
+
// recoverable instead of a silent half-write.
|
|
155
|
+
const cause = error instanceof OxygenError
|
|
156
|
+
? error.code
|
|
157
|
+
: error instanceof Error
|
|
158
|
+
? error.message
|
|
159
|
+
: "unknown";
|
|
160
|
+
const recovery = upsertKey
|
|
161
|
+
? `Re-run with --into ${newTableId} --key ${upsertKey} to resume — already-imported rows are matched by "${upsertKey}", not duplicated.`
|
|
162
|
+
: `Resume with: ${resumeCommand}`;
|
|
163
|
+
throw new OxygenError("bundle_import_incomplete", `Bundle import stopped after ${processed}/${bundle.rowCount} rows. ${recovery}`, {
|
|
164
|
+
details: {
|
|
165
|
+
table_id: newTableId,
|
|
166
|
+
table_slug: newTableSlug,
|
|
167
|
+
rows_total: bundle.rowCount,
|
|
168
|
+
rows_processed: processed,
|
|
169
|
+
failed_batch_offset: processed,
|
|
170
|
+
failed_batch_size: batch.length,
|
|
171
|
+
mode: upsertKey ? "upsert" : "insert",
|
|
172
|
+
...(upsertKey ? { key: upsertKey } : {}),
|
|
173
|
+
resume_command: resumeCommand,
|
|
174
|
+
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
175
|
+
cause,
|
|
176
|
+
},
|
|
177
|
+
exitCode: 1,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
table_id: newTableId,
|
|
183
|
+
table_slug: newTableSlug ?? null,
|
|
184
|
+
created_table: createdTable,
|
|
185
|
+
column_count: columns.length,
|
|
186
|
+
row_count: processed,
|
|
187
|
+
mode: upsertKey ? "upsert" : "insert",
|
|
188
|
+
...(skippedRelationColumns.length > 0
|
|
189
|
+
? {
|
|
190
|
+
skipped_relation_columns: skippedRelationColumns,
|
|
191
|
+
note: `${skippedRelationColumns.length} native relation column(s) were not imported: a relation must be created on this side with \`tables relate\` once both tables exist.`,
|
|
192
|
+
}
|
|
193
|
+
: {}),
|
|
194
|
+
...(upsertKey
|
|
195
|
+
? { upsert_key: upsertKey, inserted_count: insertedCount, updated_count: updatedCount }
|
|
196
|
+
: {}),
|
|
197
|
+
source_table: bundle.tableSummary,
|
|
198
|
+
schema_version: bundle.schemaVersion,
|
|
199
|
+
web_url: tableWebUrl(newTableSlug ?? newTableId),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Builds the exact CLI command that resumes a failed `import-bundle` into the
|
|
203
|
+
// table it already created. Always targets that table with --into + --key so the
|
|
204
|
+
// resume is idempotent; falls back to a <uniqueColumn> placeholder when the
|
|
205
|
+
// original run had no key (insert mode can't be safely auto-resumed).
|
|
206
|
+
export function buildBundleResumeCommand(input) {
|
|
207
|
+
const fileArg = /\s/.test(input.file) ? `"${input.file}"` : input.file;
|
|
208
|
+
const parts = [
|
|
209
|
+
// Must be the binary the user actually invoked. Hardcoding "oxygen" meant an
|
|
210
|
+
// `oxygen-dev` import printed a resume command that, pasted verbatim, re-ran
|
|
211
|
+
// a DEV bundle against PRODUCTION (OXY-4360).
|
|
212
|
+
`${input.binaryName} tables import-bundle`,
|
|
213
|
+
`--file ${fileArg}`,
|
|
214
|
+
`--into ${input.tableId}`,
|
|
215
|
+
`--key ${input.key ?? "<uniqueColumn>"}`,
|
|
216
|
+
];
|
|
217
|
+
// 500 is the default in normalizeImportBatchSize; only echo --batch-size when
|
|
218
|
+
// the user picked something else, to keep the resume command minimal.
|
|
219
|
+
if (input.batchSize !== 500) {
|
|
220
|
+
parts.push(`--batch-size ${input.batchSize}`);
|
|
221
|
+
}
|
|
222
|
+
return parts.join(" ");
|
|
223
|
+
}
|
|
224
|
+
export function toBundleColumn(column) {
|
|
225
|
+
// Persist only the fields `tables create` knows how to ingest. Drop ids,
|
|
226
|
+
// physical column names, archivedAt — those are tenant-local and would
|
|
227
|
+
// collide on import.
|
|
228
|
+
return {
|
|
229
|
+
key: column.key,
|
|
230
|
+
...(column.label !== undefined ? { label: column.label } : {}),
|
|
231
|
+
...(column.dataType !== undefined ? { dataType: column.dataType } : {}),
|
|
232
|
+
...(column.kind !== undefined ? { kind: column.kind } : {}),
|
|
233
|
+
...(column.semanticType !== undefined ? { semanticType: column.semanticType } : {}),
|
|
234
|
+
...(column.definition !== undefined ? { definition: column.definition } : {}),
|
|
235
|
+
...(column.position !== undefined ? { position: column.position } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export function bundleColumnToCreateInput(column) {
|
|
239
|
+
const label = typeof column.label === "string" && column.label.trim()
|
|
240
|
+
? column.label.trim()
|
|
241
|
+
: typeof column.key === "string"
|
|
242
|
+
? column.key
|
|
243
|
+
: null;
|
|
244
|
+
if (!label) {
|
|
245
|
+
throw new OxygenError("invalid_bundle", "Bundle column is missing label/key.", {
|
|
246
|
+
details: { column },
|
|
247
|
+
exitCode: 1,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
label,
|
|
252
|
+
...(typeof column.key === "string" ? { key: column.key } : {}),
|
|
253
|
+
...(typeof column.dataType === "string" ? { dataType: column.dataType } : {}),
|
|
254
|
+
...(typeof column.kind === "string" ? { kind: column.kind } : {}),
|
|
255
|
+
...(typeof column.semanticType === "string" ? { semanticType: column.semanticType } : {}),
|
|
256
|
+
...(column.definition && typeof column.definition === "object" && !Array.isArray(column.definition)
|
|
257
|
+
? { definition: column.definition }
|
|
258
|
+
: {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ansi } from "../../command-support.js";
|
|
2
|
+
import { TablesLinkOptions, TablesTidySuggestOptions } from "./options.js";
|
|
3
|
+
export type TableLinkCandidateResponse = {
|
|
4
|
+
matchExplanation?: string;
|
|
5
|
+
coverage?: number;
|
|
6
|
+
matchedRows?: number;
|
|
7
|
+
ambiguousRows?: number;
|
|
8
|
+
unmatchedRows?: number;
|
|
9
|
+
source?: {
|
|
10
|
+
columnLabel?: string;
|
|
11
|
+
};
|
|
12
|
+
target?: {
|
|
13
|
+
columnLabel?: string;
|
|
14
|
+
};
|
|
15
|
+
samplePairs?: Array<{
|
|
16
|
+
key?: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
export type TableLinkResponse = {
|
|
20
|
+
dry_run?: boolean;
|
|
21
|
+
cardinalitySentence?: string;
|
|
22
|
+
chosen?: TableLinkCandidateResponse | null;
|
|
23
|
+
alternatives?: TableLinkCandidateResponse[];
|
|
24
|
+
sampled?: boolean;
|
|
25
|
+
source?: {
|
|
26
|
+
tableLabel?: string;
|
|
27
|
+
rowCount?: number;
|
|
28
|
+
scannedRows?: number;
|
|
29
|
+
};
|
|
30
|
+
target?: {
|
|
31
|
+
tableLabel?: string;
|
|
32
|
+
rowCount?: number;
|
|
33
|
+
};
|
|
34
|
+
relationStorage?: "tables" | "crm_legacy";
|
|
35
|
+
registersEntity?: boolean;
|
|
36
|
+
requiresDefinition?: boolean;
|
|
37
|
+
existingRelationshipSlug?: string | null;
|
|
38
|
+
blockers?: Array<{
|
|
39
|
+
code?: string;
|
|
40
|
+
message?: string;
|
|
41
|
+
}>;
|
|
42
|
+
next_action?: string;
|
|
43
|
+
web_url?: string;
|
|
44
|
+
action_run_id?: string;
|
|
45
|
+
workspace_run_id?: string;
|
|
46
|
+
archived_edge_count?: number;
|
|
47
|
+
edges_to_archive?: number;
|
|
48
|
+
};
|
|
49
|
+
export declare function linkBar(coverage: number, width?: number): string;
|
|
50
|
+
export declare function writeTableLinkPreview(table: string, data: TableLinkResponse, options: TablesLinkOptions): void;
|
|
51
|
+
export type TidyDuplicateSuggestion = {
|
|
52
|
+
survivorColumnId?: string;
|
|
53
|
+
survivorKey?: string;
|
|
54
|
+
loserColumnId?: string;
|
|
55
|
+
loserKey?: string;
|
|
56
|
+
similarity?: number;
|
|
57
|
+
dataType?: string;
|
|
58
|
+
sampledOverlap?: number;
|
|
59
|
+
loserHasUniqueValues?: boolean;
|
|
60
|
+
recommendation?: string;
|
|
61
|
+
};
|
|
62
|
+
export type TidyFormattingIssue = {
|
|
63
|
+
columnId?: string;
|
|
64
|
+
columnKey?: string;
|
|
65
|
+
issue?: string;
|
|
66
|
+
sampleRowIds?: string[];
|
|
67
|
+
detail?: string;
|
|
68
|
+
};
|
|
69
|
+
export type TidyHideSuggestion = {
|
|
70
|
+
columnId?: string;
|
|
71
|
+
columnKey?: string;
|
|
72
|
+
reason?: string;
|
|
73
|
+
fillRate?: number;
|
|
74
|
+
suggestedPosition?: number | null;
|
|
75
|
+
};
|
|
76
|
+
export type TidyValueSanityIssue = {
|
|
77
|
+
columnId?: string;
|
|
78
|
+
columnKey?: string;
|
|
79
|
+
rowId?: string;
|
|
80
|
+
value?: unknown;
|
|
81
|
+
reason?: string;
|
|
82
|
+
};
|
|
83
|
+
export type TidyNotComputedColumn = {
|
|
84
|
+
columnId?: string;
|
|
85
|
+
columnKey?: string;
|
|
86
|
+
reason?: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
};
|
|
89
|
+
export type TidySuggestResponse = {
|
|
90
|
+
tableId?: string;
|
|
91
|
+
sampledRows?: number;
|
|
92
|
+
rowCount?: number;
|
|
93
|
+
duplicateColumns?: TidyDuplicateSuggestion[];
|
|
94
|
+
formattingIssues?: TidyFormattingIssue[];
|
|
95
|
+
hideSuggestions?: TidyHideSuggestion[];
|
|
96
|
+
valueSanity?: TidyValueSanityIssue[];
|
|
97
|
+
notComputed?: TidyNotComputedColumn[];
|
|
98
|
+
url?: string;
|
|
99
|
+
web_url?: string;
|
|
100
|
+
deepLink?: string;
|
|
101
|
+
};
|
|
102
|
+
export declare function handleTablesTidySuggestAction(table: string, options: TablesTidySuggestOptions): Promise<void>;
|
|
103
|
+
export declare function formatTidyCount(value: unknown): string;
|
|
104
|
+
export declare function formatTidyPercent(value: unknown): string;
|
|
105
|
+
export declare function pushTidySection<T>(lines: string[], styles: ReturnType<typeof ansi>, title: string, items: T[], renderItem: (item: T) => string): void;
|
|
106
|
+
export declare function formatTidySuggestions(data: TidySuggestResponse): string;
|
|
107
|
+
export declare function writeRowDeleteWarnings(data: unknown): void;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { writeJson, emitCliFailure, asPayloadRecord } from "../../cli-output.js";
|
|
2
|
+
import { readPositiveInt } from "../../cli-values.js";
|
|
3
|
+
import { readCsvOption, ansi } from "../../command-support.js";
|
|
4
|
+
import { requestOxygen } from "../../http-client.js";
|
|
5
|
+
import { readOption } from "../../util.js";
|
|
6
|
+
import { success } from "@oxygen/shared/cli-result";
|
|
7
|
+
import { stdout as output } from "node:process";
|
|
8
|
+
export function linkBar(coverage, width = 36) {
|
|
9
|
+
const filled = Math.max(0, Math.min(width, Math.round(coverage * width)));
|
|
10
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
11
|
+
}
|
|
12
|
+
export function writeTableLinkPreview(table, data, options) {
|
|
13
|
+
const out = (line = "") => process.stdout.write(`${line}\n`);
|
|
14
|
+
// Undo has no plan to render — just the count and what it touched.
|
|
15
|
+
if (typeof data.edges_to_archive === "number" || typeof data.archived_edge_count === "number") {
|
|
16
|
+
if (typeof data.archived_edge_count === "number") {
|
|
17
|
+
out(`Undone — ${data.archived_edge_count.toLocaleString()} links archived.`);
|
|
18
|
+
out("Links are archived, not deleted, and only the ones that run created were touched.");
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
out(`This will archive ${(data.edges_to_archive ?? 0).toLocaleString()} links created by that run.`);
|
|
22
|
+
out(`Apply: oxygen tables link ${table} --undo ${readOption(options.undo) ?? "<run_id>"} --approved`);
|
|
23
|
+
}
|
|
24
|
+
if (data.web_url)
|
|
25
|
+
out(data.web_url);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const blockers = data.blockers ?? [];
|
|
29
|
+
if (blockers.length > 0) {
|
|
30
|
+
for (const blocker of blockers)
|
|
31
|
+
out(blocker.message ?? blocker.code ?? "This link cannot run yet.");
|
|
32
|
+
if (data.web_url)
|
|
33
|
+
out(data.web_url);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Approved run: it is queued, so print how to watch it, not the plan again.
|
|
37
|
+
// The two follow-ups take DIFFERENT ids and that is not cosmetic — undo
|
|
38
|
+
// archives by edge run_id, which is the parent workspace run.
|
|
39
|
+
const watchId = data.action_run_id ?? data.workspace_run_id;
|
|
40
|
+
const undoId = data.workspace_run_id;
|
|
41
|
+
if (!data.dry_run && watchId) {
|
|
42
|
+
out(`Linking rows in the background.`);
|
|
43
|
+
if (data.web_url)
|
|
44
|
+
out(`Run: ${data.web_url}`);
|
|
45
|
+
out(`Watch: oxygen table-runs wait ${watchId}`);
|
|
46
|
+
if (undoId)
|
|
47
|
+
out(`Undo: oxygen tables link ${table} --undo ${undoId}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const chosen = data.chosen ?? null;
|
|
51
|
+
const sourceLabel = data.source?.tableLabel ?? table;
|
|
52
|
+
const targetLabel = data.target?.tableLabel ?? readOption(options.to) ?? "the other table";
|
|
53
|
+
out(`Linking "${sourceLabel}" -> "${targetLabel}"`);
|
|
54
|
+
out();
|
|
55
|
+
if (!chosen) {
|
|
56
|
+
out(" No column in these two tables matches well enough to link them.");
|
|
57
|
+
out(" Add a shared value to both — a website, a domain, a LinkedIn URL, or an email.");
|
|
58
|
+
if (data.web_url)
|
|
59
|
+
out(data.web_url);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const pad = (label) => label.padEnd(14);
|
|
63
|
+
out(` ${pad("Matching on")}${chosen.source?.columnLabel ?? "?"} -> ${chosen.target?.columnLabel ?? "?"}`);
|
|
64
|
+
if (chosen.matchExplanation)
|
|
65
|
+
out(` ${pad("")}(${chosen.matchExplanation})`);
|
|
66
|
+
if (data.cardinalitySentence)
|
|
67
|
+
out(` ${pad("Direction")}${data.cardinalitySentence}`);
|
|
68
|
+
const scanned = data.source?.scannedRows ?? 0;
|
|
69
|
+
const total = data.source?.rowCount ?? scanned;
|
|
70
|
+
// Always name the sample. `sampled` means the scan hit its bound, so these are
|
|
71
|
+
// estimates over a prefix of the table, not the whole thing.
|
|
72
|
+
if (data.sampled && scanned < total) {
|
|
73
|
+
out(` ${pad("Measured on")}${scanned.toLocaleString()} of ${total.toLocaleString()} rows`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
out(` ${pad("Measured on")}all ${total.toLocaleString()} rows`);
|
|
77
|
+
}
|
|
78
|
+
out();
|
|
79
|
+
const coverage = chosen.coverage ?? 0;
|
|
80
|
+
out(` ${linkBar(coverage)} ${(chosen.matchedRows ?? 0).toLocaleString()} will link ${Math.round(coverage * 100)}%`);
|
|
81
|
+
if ((chosen.ambiguousRows ?? 0) > 0) {
|
|
82
|
+
out(` ${" ".repeat(38)}${(chosen.ambiguousRows ?? 0).toLocaleString()} ambiguous (skipped)`);
|
|
83
|
+
}
|
|
84
|
+
if ((chosen.unmatchedRows ?? 0) > 0) {
|
|
85
|
+
out(` ${" ".repeat(38)}${(chosen.unmatchedRows ?? 0).toLocaleString()} with no match yet`);
|
|
86
|
+
}
|
|
87
|
+
out();
|
|
88
|
+
const samples = chosen.samplePairs ?? [];
|
|
89
|
+
if (samples.length > 0) {
|
|
90
|
+
for (const sample of samples.slice(0, 3))
|
|
91
|
+
out(` matched on "${sample.key ?? ""}"`);
|
|
92
|
+
if (samples.length > 3)
|
|
93
|
+
out(` ... ${samples.length - 3} more`);
|
|
94
|
+
out();
|
|
95
|
+
}
|
|
96
|
+
if ((chosen.ambiguousRows ?? 0) > 0) {
|
|
97
|
+
out(` Ambiguous rows match more than one row on the other side, so they are left`);
|
|
98
|
+
out(` unlinked rather than guessed. Fix the duplicates first:`);
|
|
99
|
+
out(` oxygen tables dedupe ${targetLabel} --on <column>`);
|
|
100
|
+
out();
|
|
101
|
+
}
|
|
102
|
+
if ((chosen.unmatchedRows ?? 0) > 0 && !options.createMissing) {
|
|
103
|
+
out(` Add --create-missing to create the missing target rows too (free).`);
|
|
104
|
+
out();
|
|
105
|
+
}
|
|
106
|
+
const alternatives = data.alternatives ?? [];
|
|
107
|
+
if (alternatives.length > 0) {
|
|
108
|
+
const best = alternatives[0];
|
|
109
|
+
if (best?.source?.columnLabel) {
|
|
110
|
+
out(` Also measured: ${best.source.columnLabel} -> ${best.target?.columnLabel ?? "?"} ${Math.round((best.coverage ?? 0) * 100)}%`);
|
|
111
|
+
out(` retry with --on "${best.source.columnLabel}"`);
|
|
112
|
+
out();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (data.relationStorage === "tables") {
|
|
116
|
+
out(` Stored in Tables — neither table is registered as a CRM object.`);
|
|
117
|
+
out();
|
|
118
|
+
}
|
|
119
|
+
out(" Free — 0 credits. Nothing has been written.");
|
|
120
|
+
const applyFlags = [`--to ${readOption(options.to) ?? "<table>"}`];
|
|
121
|
+
if (readOption(options.on))
|
|
122
|
+
applyFlags.push(`--on "${readOption(options.on)}"`);
|
|
123
|
+
if (options.createMissing)
|
|
124
|
+
applyFlags.push("--create-missing");
|
|
125
|
+
out(` Apply: oxygen tables link ${table} ${applyFlags.join(" ")} --approved`);
|
|
126
|
+
if (data.web_url)
|
|
127
|
+
out(` ${data.web_url}`);
|
|
128
|
+
}
|
|
129
|
+
// `tables tidy-suggest` is a read-only analysis (no credits, no writes): it
|
|
130
|
+
// surfaces duplicate columns, formatting inconsistencies, hide candidates, and
|
|
131
|
+
// value-sanity findings so the caller can pick which reversible cleanup to run
|
|
132
|
+
// next. --json returns the raw report envelope; the default rendering compacts
|
|
133
|
+
// the four sections for a terminal reader and echoes the deep-link.
|
|
134
|
+
export async function handleTablesTidySuggestAction(table, options) {
|
|
135
|
+
try {
|
|
136
|
+
const limit = readPositiveInt(options.limit);
|
|
137
|
+
const categories = readCsvOption(options.categories);
|
|
138
|
+
const data = await requestOxygen("/api/cli/tables/tidy/suggest", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: {
|
|
141
|
+
table,
|
|
142
|
+
...(limit ? { limit } : {}),
|
|
143
|
+
...(categories.length ? { categories } : {}),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (options.json) {
|
|
147
|
+
writeJson(success("tables tidy-suggest", data));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write(formatTidySuggestions(data));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
emitCliFailure("tables tidy-suggest", error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function formatTidyCount(value) {
|
|
157
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "0";
|
|
158
|
+
}
|
|
159
|
+
export function formatTidyPercent(value) {
|
|
160
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
161
|
+
return "—";
|
|
162
|
+
return `${(value * 100).toFixed(0)}%`;
|
|
163
|
+
}
|
|
164
|
+
// Render one tidy-suggest section (header + count, then either a dim "None" or
|
|
165
|
+
// one formatted line per item) into the shared `lines` buffer. Extracted so the
|
|
166
|
+
// four sections share a single control-flow shape instead of repeating it.
|
|
167
|
+
export function pushTidySection(lines, styles, title, items, renderItem) {
|
|
168
|
+
lines.push("", styles.bold(`${title} (${items.length})`));
|
|
169
|
+
if (items.length === 0) {
|
|
170
|
+
lines.push(` ${styles.dim("None")}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
lines.push(renderItem(item));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function formatTidySuggestions(data) {
|
|
178
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
179
|
+
const duplicates = Array.isArray(data.duplicateColumns) ? data.duplicateColumns : [];
|
|
180
|
+
const formatting = Array.isArray(data.formattingIssues) ? data.formattingIssues : [];
|
|
181
|
+
const hide = Array.isArray(data.hideSuggestions) ? data.hideSuggestions : [];
|
|
182
|
+
const valueSanity = Array.isArray(data.valueSanity) ? data.valueSanity : [];
|
|
183
|
+
const notComputed = Array.isArray(data.notComputed) ? data.notComputed : [];
|
|
184
|
+
const lines = [
|
|
185
|
+
"",
|
|
186
|
+
`${styles.bold("Table tidy suggestions")} ${styles.dim(`(sampled ${formatTidyCount(data.sampledRows)} of ${formatTidyCount(data.rowCount)} rows)`)}`,
|
|
187
|
+
];
|
|
188
|
+
pushTidySection(lines, styles, "Duplicate columns", duplicates, (dup) => {
|
|
189
|
+
const loser = dup.loserKey ?? dup.loserColumnId ?? "?";
|
|
190
|
+
const survivor = dup.survivorKey ?? dup.survivorColumnId ?? "?";
|
|
191
|
+
return ` ${loser} ${styles.dim("→ duplicate of")} ${survivor} ${styles.dim(`(${formatTidyPercent(dup.similarity)} match, ${dup.recommendation ?? "review"})`)}`;
|
|
192
|
+
});
|
|
193
|
+
pushTidySection(lines, styles, "Formatting issues", formatting, (issue) => {
|
|
194
|
+
const sampleCount = Array.isArray(issue.sampleRowIds) ? issue.sampleRowIds.length : 0;
|
|
195
|
+
const column = issue.columnKey ?? issue.columnId ?? "?";
|
|
196
|
+
return ` ${column} ${styles.dim(`— ${issue.issue ?? "issue"}`)}${issue.detail ? `: ${issue.detail}` : ""} ${styles.dim(`(${sampleCount} sample row(s))`)}`;
|
|
197
|
+
});
|
|
198
|
+
pushTidySection(lines, styles, "Hide suggestions", hide, (suggestion) => {
|
|
199
|
+
const column = suggestion.columnKey ?? suggestion.columnId ?? "?";
|
|
200
|
+
return ` ${column} ${styles.dim(`— ${suggestion.reason ?? "hide"} (${formatTidyPercent(suggestion.fillRate)} filled)`)}`;
|
|
201
|
+
});
|
|
202
|
+
pushTidySection(lines, styles, "Value-sanity findings", valueSanity, (finding) => {
|
|
203
|
+
const column = finding.columnKey ?? finding.columnId ?? "?";
|
|
204
|
+
return ` ${column} ${styles.dim(`row ${finding.rowId ?? "?"} — ${finding.reason ?? "check value"}`)}`;
|
|
205
|
+
});
|
|
206
|
+
// Printed even when empty, like the four finding sections: a reader who never
|
|
207
|
+
// sees this heading cannot tell a measured column from an unmeasured one.
|
|
208
|
+
pushTidySection(lines, styles, "Not measured", notComputed, (entry) => {
|
|
209
|
+
const column = entry.columnKey ?? entry.columnId ?? "?";
|
|
210
|
+
return ` ${column} ${styles.dim(`— ${entry.message ?? entry.reason ?? "could not be measured"}`)}`;
|
|
211
|
+
});
|
|
212
|
+
const link = data.deepLink ?? data.web_url ?? data.url;
|
|
213
|
+
if (link) {
|
|
214
|
+
lines.push("", styles.dim(`View table: ${link}`));
|
|
215
|
+
}
|
|
216
|
+
lines.push("");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
// `tables delete-row|delete-rows` for a human reader: a deleted row's lead stays
|
|
220
|
+
// enrolled in its campaigns, so name them where the delete was typed.
|
|
221
|
+
export function writeRowDeleteWarnings(data) {
|
|
222
|
+
const payload = asPayloadRecord(data);
|
|
223
|
+
const warnings = Array.isArray(payload?.warnings) ? payload.warnings : [];
|
|
224
|
+
for (const entry of warnings) {
|
|
225
|
+
const warning = asPayloadRecord(entry);
|
|
226
|
+
if (warning && typeof warning.message === "string")
|
|
227
|
+
process.stderr.write(`Warning: ${warning.message}\n`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ImportBatchWriteSummary = {
|
|
2
|
+
rowCount: number;
|
|
3
|
+
insertedCount: number;
|
|
4
|
+
updatedCount: number;
|
|
5
|
+
warningCount: number;
|
|
6
|
+
warnings: unknown[];
|
|
7
|
+
warningsTruncated: boolean;
|
|
8
|
+
writeRequestCount: number;
|
|
9
|
+
requestTooLargeRetries: number;
|
|
10
|
+
byteSplits: number;
|
|
11
|
+
minimumBatchSizeUsed: number;
|
|
12
|
+
lastResult: Record<string, unknown> | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function writeImportBatchWithAutoSplit(input: {
|
|
15
|
+
tableRef: string;
|
|
16
|
+
upsertKey: string | undefined;
|
|
17
|
+
rows: Record<string, unknown>[];
|
|
18
|
+
requestId?: string;
|
|
19
|
+
}): Promise<ImportBatchWriteSummary>;
|
|
20
|
+
export declare function writeImportBatchHalves(input: Parameters<typeof writeImportBatchWithAutoSplit>[0], midpoint: number): Promise<[ImportBatchWriteSummary, ImportBatchWriteSummary]>;
|
|
21
|
+
export declare function summarizeImportBatchWrite(result: Record<string, unknown>, batchSize: number): ImportBatchWriteSummary;
|
|
22
|
+
export declare function combineImportBatchWriteSummaries(first: ImportBatchWriteSummary, second: ImportBatchWriteSummary, extra: {
|
|
23
|
+
requestTooLargeRetries?: number;
|
|
24
|
+
byteSplits?: number;
|
|
25
|
+
}): ImportBatchWriteSummary;
|
|
26
|
+
export declare function isRequestTooLargeError(error: unknown): boolean;
|