@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
710 lines • 30.8 kB
JavaScript
import { isString } from 'es-toolkit/compat';
import { fetchWithTimeout } from '../search/providers/request.js';
import { getCommandErrorMessage, runGcloudFileCommand } from '../../utils/gcloud.js';
import { dedupeMessages, normalizeOptionalString } from '../../utils/value.js';
const BIGQUERY_API_BASE_URL = 'https://bigquery.googleapis.com/bigquery/v2';
const BIGQUERY_CONFLICT_STATUS = 409;
const BIGQUERY_NOT_FOUND_STATUS = 404;
const BIGQUERY_REQUEST_TIMEOUT_MS = 10_000;
const BIGQUERY_TYPE_ALIASES = {
bignumeric: 'BIGNUMERIC',
bool: 'BOOL',
boolean: 'BOOL',
bytes: 'BYTES',
date: 'DATE',
datetime: 'DATETIME',
float: 'FLOAT64',
float64: 'FLOAT64',
geography: 'GEOGRAPHY',
int64: 'INT64',
integer: 'INT64',
json: 'JSON',
numeric: 'NUMERIC',
string: 'STRING',
time: 'TIME',
timestamp: 'TIMESTAMP'
};
const BIGQUERY_MODE_ALIASES = {
nullable: 'NULLABLE',
repeated: 'REPEATED',
required: 'REQUIRED'
};
const normalizeBigQueryFieldType = value => {
const normalizedValue = normalizeOptionalString(value)?.toLowerCase();
return BIGQUERY_TYPE_ALIASES[normalizedValue] ?? normalizedValue?.toUpperCase() ?? null;
};
const normalizeBigQueryFieldMode = value => {
const normalizedValue = normalizeOptionalString(value)?.toLowerCase();
return BIGQUERY_MODE_ALIASES[normalizedValue] ?? normalizedValue?.toUpperCase() ?? 'NULLABLE';
};
const normalizeBigQuerySchemaField = fieldConfig => {
const fieldName = normalizeOptionalString(fieldConfig?.name);
if (!fieldName) {
return null;
}
return {
...(normalizeOptionalString(fieldConfig?.description) ? {
description: normalizeOptionalString(fieldConfig.description)
} : {}),
mode: normalizeBigQueryFieldMode(fieldConfig?.mode),
name: fieldName,
type: normalizeBigQueryFieldType(fieldConfig?.type)
};
};
const createBigQueryFieldMap = fields => new Map((fields ?? []).map(normalizeBigQuerySchemaField).filter(Boolean).map(field => [field.name, field]));
const getSortedFieldNames = fieldMap => [...fieldMap.keys()].sort((left, right) => left.localeCompare(right));
const createBigQueryApiUrl = requestPath => `${BIGQUERY_API_BASE_URL}${requestPath}`;
const parseBigQueryResponseBody = async response => {
const responseText = await response.text();
if (!responseText) {
return null;
}
try {
return JSON.parse(responseText);
} catch {
return responseText;
}
};
const getBigQueryErrorDetail = responseBody => {
if (isString(responseBody) && responseBody.trim().length > 0) {
return responseBody.trim();
}
return normalizeOptionalString(responseBody?.error?.message) ?? 'BigQuery returned an unknown error.';
};
const getBigQueryAccessToken = (dependencies = {}) => {
if (typeof dependencies.getBigQueryAccessToken === 'function') {
return dependencies.getBigQueryAccessToken();
}
const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? ((args, options) => runGcloudFileCommand(args, options, dependencies.runCommand));
return normalizeOptionalString(runGcloudFileCommandImpl(['auth', 'print-access-token'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}));
};
const requestBigQuery = async (requestPath, options = {}, dependencies = {}) => {
const response = await fetchWithTimeout(createBigQueryApiUrl(requestPath), {
method: options.method ?? 'GET',
body: options.body ? JSON.stringify(options.body) : undefined,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${options.accessToken}`,
...(options.body ? {
'Content-Type': 'application/json'
} : {})
}
}, options.timeoutMs ?? BIGQUERY_REQUEST_TIMEOUT_MS, {
fetchImpl: dependencies.fetchImpl
});
const responseBody = await parseBigQueryResponseBody(response);
return {
body: responseBody,
ok: response.ok,
status: response.status
};
};
const createBigQueryOperationError = (operation, targetDescription, detail) => new Error(`Could not ${operation} ${targetDescription}. ${detail} ` + 'Check Google Cloud authentication and BigQuery access before retrying "atlas sync deploy".');
const readBigQueryDataset = async (projectId, datasetId, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}`, {
accessToken
}, dependencies);
if (response.status === BIGQUERY_NOT_FOUND_STATUS) {
return null;
}
if (!response.ok) {
throw createBigQueryOperationError('inspect BigQuery dataset', `${projectId}:${datasetId}`, getBigQueryErrorDetail(response.body));
}
return response.body;
};
const readBigQueryTable = async (projectId, datasetId, tableId, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables/${encodeURIComponent(tableId)}`, {
accessToken
}, dependencies);
if (response.status === BIGQUERY_NOT_FOUND_STATUS) {
return null;
}
if (!response.ok) {
throw createBigQueryOperationError('inspect BigQuery table', `${projectId}:${datasetId}.${tableId}`, getBigQueryErrorDetail(response.body));
}
return response.body;
};
const createBigQueryDataset = async (projectId, datasetId, datasetLocation, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets`, {
accessToken,
method: 'POST',
body: {
datasetReference: {
datasetId,
projectId
},
location: datasetLocation
}
}, dependencies);
if (response.ok || response.status === BIGQUERY_CONFLICT_STATUS) {
return response.body;
}
throw createBigQueryOperationError('create BigQuery dataset', `${projectId}:${datasetId}`, getBigQueryErrorDetail(response.body));
};
const createBigQueryTable = async (projectId, datasetId, tableId, schemaFields, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables`, {
accessToken,
method: 'POST',
body: {
schema: {
fields: schemaFields
},
tableReference: {
datasetId,
projectId,
tableId
}
}
}, dependencies);
if (response.ok || response.status === BIGQUERY_CONFLICT_STATUS) {
return response.body;
}
throw createBigQueryOperationError('create BigQuery table', `${projectId}:${datasetId}.${tableId}`, getBigQueryErrorDetail(response.body));
};
const updateBigQueryTableSchema = async (projectId, datasetId, tableId, schemaFields, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables/${encodeURIComponent(tableId)}`, {
accessToken,
method: 'PATCH',
body: {
schema: {
fields: schemaFields
}
}
}, dependencies);
if (response.ok) {
return response.body;
}
throw createBigQueryOperationError('update BigQuery table schema', `${projectId}:${datasetId}.${tableId}`, getBigQueryErrorDetail(response.body));
};
const createBigQueryView = async (projectId, datasetId, tableId, query, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables`, {
accessToken,
method: 'POST',
body: {
tableReference: {
datasetId,
projectId,
tableId
},
view: {
query,
useLegacySql: false
}
}
}, dependencies);
if (response.ok || response.status === BIGQUERY_CONFLICT_STATUS) {
return response.body;
}
throw createBigQueryOperationError('create BigQuery view', `${projectId}:${datasetId}.${tableId}`, getBigQueryErrorDetail(response.body));
};
const updateBigQueryView = async (projectId, datasetId, tableId, query, accessToken, dependencies = {}) => {
const response = await requestBigQuery(`/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables/${encodeURIComponent(tableId)}`, {
accessToken,
method: 'PATCH',
body: {
view: {
query,
useLegacySql: false
}
}
}, dependencies);
if (response.ok) {
return response.body;
}
throw createBigQueryOperationError('update BigQuery view', `${projectId}:${datasetId}.${tableId}`, getBigQueryErrorDetail(response.body));
};
const createEntryLabel = entry => `${entry.destinationName} (${entry.target}: ${entry.dataset}.${entry.table})`;
const createReadAliasEntryLabel = entry => `${entry.destinationName} (read alias: ${entry.dataset}.${entry.table})`;
const createTableMismatchMessage = (entry, detail) => `${createEntryLabel(entry)} does not match the authored schema. ${detail}`;
const createReadAliasMismatchMessage = (entry, detail) => `${createReadAliasEntryLabel(entry)} does not match the expected live read target. ${detail}`;
const normalizeBigQueryQuery = query => normalizeOptionalString(query)?.replace(/\s+/g, ' ') ?? null;
const createBigQueryReadAliasQuery = entry => `SELECT * FROM \`${entry.projectId}.${entry.targetDataset}.${entry.targetTable}\``;
const isBigQueryViewResource = tableResource => [tableResource?.tableType, tableResource?.type].map(value => normalizeOptionalString(value)?.toUpperCase()).includes('VIEW');
const compareBigQuerySchemas = (expectedFields, remoteFields, entry) => {
const expectedFieldMap = createBigQueryFieldMap(expectedFields);
const remoteFieldMap = createBigQueryFieldMap(remoteFields);
const expectedFieldNames = getSortedFieldNames(expectedFieldMap);
const remoteFieldNames = getSortedFieldNames(remoteFieldMap);
const blockingIssues = [];
const mismatchDetails = [];
const addFieldActions = [];
const unexpectedRemoteFields = remoteFieldNames.filter(fieldName => !expectedFieldMap.has(fieldName));
if (unexpectedRemoteFields.length > 0) {
mismatchDetails.push(`unexpected fields: ${unexpectedRemoteFields.join(', ')}`);
blockingIssues.push(createTableMismatchMessage(entry, `Unexpected remote fields were found: ${unexpectedRemoteFields.join(', ')}.`));
}
for (const fieldName of expectedFieldNames) {
const expectedField = expectedFieldMap.get(fieldName);
const remoteField = remoteFieldMap.get(fieldName);
if (!expectedField) {
continue;
}
if (!remoteField) {
if (expectedField.mode === 'REQUIRED') {
mismatchDetails.push(`missing required field: ${fieldName}`);
blockingIssues.push(createTableMismatchMessage(entry, `Remote table is missing required field ${fieldName}, which cannot be added safely to an existing BigQuery table.`));
continue;
}
addFieldActions.push({
action: 'add-field',
field: expectedField,
label: `Add field ${fieldName}`
});
mismatchDetails.push(`missing field: ${fieldName}`);
continue;
}
if (expectedField.type !== remoteField.type) {
mismatchDetails.push(`field ${fieldName}: expected type ${expectedField.type}, received ${remoteField.type}`);
blockingIssues.push(createTableMismatchMessage(entry, `Field ${fieldName} expects type ${expectedField.type} but the remote table uses ${remoteField.type}.`));
}
if (expectedField.mode !== remoteField.mode) {
mismatchDetails.push(`field ${fieldName}: expected mode ${expectedField.mode}, received ${remoteField.mode}`);
blockingIssues.push(createTableMismatchMessage(entry, `Field ${fieldName} expects mode ${expectedField.mode} but the remote table uses ${remoteField.mode}.`));
}
}
return {
actions: addFieldActions,
blockingIssues,
mismatchDetails,
schemaStatus: blockingIssues.length > 0 ? 'mismatch' : addFieldActions.length > 0 ? 'pending-update' : 'match'
};
};
const createBigQueryTargetEntries = (runtimeConfig, context) => {
const releaseStrategy = normalizeOptionalString(runtimeConfig.release?.strategy) ?? 'managed';
const datasetLocation = normalizeOptionalString(context.config?.deploy?.cloudRun?.region) ?? null;
return Object.values(runtimeConfig.destinations ?? {}).filter(destinationConfig => destinationConfig?.type === 'bigquery').filter(destinationConfig => normalizeOptionalString(destinationConfig.schemaMode) === 'managed').filter(destinationConfig => destinationConfig.bigquery?.schema?.mode === 'explicit').flatMap(destinationConfig => {
const targetKeys = releaseStrategy === 'managed' ? ['active', 'candidate'] : ['direct'];
return targetKeys.map(targetKey => {
const targetConfig = destinationConfig.physicalTargets?.[targetKey] ?? null;
if (!targetConfig) {
return null;
}
return {
actions: [],
blockingIssues: [],
dataset: targetConfig.dataset,
datasetLocation,
destinationName: destinationConfig.name,
mismatchDetails: [],
nextFields: (destinationConfig.bigquery?.schema?.fields ?? []).map(normalizeBigQuerySchemaField).filter(Boolean),
schemaStatus: 'unknown',
table: targetConfig.table,
target: targetKey
};
}).filter(Boolean);
});
};
const groupEntriesByDestination = entries => Object.fromEntries(Object.entries(entries.reduce((accumulator, entry) => {
if (!accumulator[entry.destinationName]) {
accumulator[entry.destinationName] = [];
}
accumulator[entry.destinationName].push(entry);
return accumulator;
}, {})).map(([destinationName, destinationEntries]) => [destinationName, destinationEntries.sort((left, right) => left.target.localeCompare(right.target))]));
const createSchemaPlanResult = (entries, warnings = [], blockingIssues = [], status = null) => {
const plannedStatus = status ?? (entries.length === 0 ? 'not-applicable' : blockingIssues.length > 0 ? 'blocked' : entries.some(entry => entry.schemaStatus === 'unknown') ? 'unknown' : entries.some(entry => entry.actions.length > 0) ? 'planned' : 'match');
return {
actions: entries.flatMap(entry => entry.actions),
blockingIssues: dedupeMessages(blockingIssues),
destinations: groupEntriesByDestination(entries),
entries,
status: plannedStatus,
warnings: dedupeMessages(warnings)
};
};
const createReadAliasPlanResult = (entries, warnings = [], blockingIssues = [], status = null) => {
const plannedStatus = status ?? (entries.length === 0 ? 'not-applicable' : blockingIssues.length > 0 ? 'blocked' : entries.some(entry => entry.aliasStatus === 'unknown') ? 'unknown' : entries.some(entry => entry.actions.length > 0) ? 'planned' : 'match');
return {
actions: entries.flatMap(entry => entry.actions),
blockingIssues: dedupeMessages(blockingIssues),
destinations: groupEntriesByDestination(entries),
entries,
status: plannedStatus,
warnings: dedupeMessages(warnings)
};
};
const createUnavailableEntry = (entry, issue, strict) => ({
...entry,
blockingIssues: strict ? [issue] : [],
schemaStatus: 'unknown'
});
const createUnavailableReadAliasEntry = (entry, issue, strict) => ({
...entry,
blockingIssues: strict ? [issue] : [],
aliasStatus: 'unknown'
});
const createBigQueryReadAliasEntries = (runtimeConfig, context) => {
const datasetLocation = normalizeOptionalString(context.config?.deploy?.cloudRun?.region) ?? null;
return Object.values(runtimeConfig.destinations ?? {}).filter(destinationConfig => destinationConfig?.type === 'bigquery').filter(destinationConfig => normalizeOptionalString(destinationConfig.schemaMode) === 'managed').map(destinationConfig => {
const aliasTarget = destinationConfig.physicalTargets?.direct ?? null;
const readTarget = destinationConfig.routing?.read ?? null;
if (!aliasTarget || !readTarget) {
return null;
}
if (aliasTarget.dataset === readTarget.dataset && aliasTarget.table === readTarget.table) {
return null;
}
return {
actions: [],
aliasStatus: 'unknown',
blockingIssues: [],
dataset: aliasTarget.dataset,
datasetLocation,
destinationName: destinationConfig.name,
expectedQuery: createBigQueryReadAliasQuery({
projectId: context.projectId,
targetDataset: readTarget.dataset,
targetTable: readTarget.table
}),
mismatchDetails: [],
projectId: context.projectId,
table: aliasTarget.table,
targetDataset: readTarget.dataset,
targetReleaseId: readTarget.releaseId ?? null,
targetRouting: readTarget.target ?? null,
targetTable: readTarget.table
};
}).filter(Boolean);
};
export const inspectSyncReadAliases = async (context, runtimeConfig, options = {}, dependencies = {}) => {
const strict = options.strict === true;
const entries = createBigQueryReadAliasEntries(runtimeConfig, context);
if (entries.length === 0) {
return createReadAliasPlanResult([], [], [], 'not-applicable');
}
let accessToken;
try {
accessToken = getBigQueryAccessToken(dependencies);
} catch (error) {
const issue = `Atlas could not resolve a Google Cloud access token for BigQuery read alias inspection. ${getCommandErrorMessage(error)}`;
return createReadAliasPlanResult(entries.map(entry => createUnavailableReadAliasEntry(entry, issue, strict)), strict ? [] : [issue], strict ? [issue] : [], strict ? 'blocked' : 'unknown');
}
if (!accessToken) {
const issue = 'Atlas could not resolve a Google Cloud access token for BigQuery read alias inspection.';
return createReadAliasPlanResult(entries.map(entry => createUnavailableReadAliasEntry(entry, issue, strict)), strict ? [] : [issue], strict ? [issue] : [], strict ? 'blocked' : 'unknown');
}
const nextEntries = [];
const warnings = [];
const blockingIssues = [];
for (const entry of entries) {
try {
const dataset = await readBigQueryDataset(context.projectId, entry.dataset, accessToken, dependencies);
if (!dataset) {
const datasetBlockingIssues = [];
if (!entry.datasetLocation) {
datasetBlockingIssues.push(`${createReadAliasEntryLabel(entry)} cannot create missing dataset ${entry.dataset}` + 'because no dataset location could be derived from the Atlas sync config.');
}
nextEntries.push({
...entry,
actions: [{
action: 'create-dataset',
dataset: entry.dataset,
location: entry.datasetLocation,
table: entry.table
}, {
action: 'create-view',
dataset: entry.dataset,
query: entry.expectedQuery,
table: entry.table
}],
aliasStatus: datasetBlockingIssues.length > 0 ? 'mismatch' : 'not-found',
blockingIssues: datasetBlockingIssues,
mismatchDetails: datasetBlockingIssues.length > 0 ? ['missing dataset location'] : ['dataset not found']
});
blockingIssues.push(...datasetBlockingIssues);
continue;
}
const remoteAlias = await readBigQueryTable(context.projectId, entry.dataset, entry.table, accessToken, dependencies);
if (!remoteAlias) {
nextEntries.push({
...entry,
actions: [{
action: 'create-view',
dataset: entry.dataset,
query: entry.expectedQuery,
table: entry.table
}],
aliasStatus: 'not-found',
mismatchDetails: ['view not found']
});
continue;
}
if (!isBigQueryViewResource(remoteAlias)) {
const issue = createReadAliasMismatchMessage(entry, `Expected a BigQuery view at ${entry.dataset}.${entry.table}, but found a non-view table resource.`);
nextEntries.push({
...entry,
aliasStatus: 'mismatch',
blockingIssues: [issue],
mismatchDetails: ['alias target is not a view']
});
blockingIssues.push(issue);
continue;
}
const normalizedRemoteQuery = normalizeBigQueryQuery(remoteAlias.view?.query);
const normalizedExpectedQuery = normalizeBigQueryQuery(entry.expectedQuery);
const usesLegacySql = remoteAlias.view?.useLegacySql === true;
if (normalizedRemoteQuery === normalizedExpectedQuery && !usesLegacySql) {
nextEntries.push({
...entry,
aliasStatus: 'match'
});
continue;
}
nextEntries.push({
...entry,
actions: [{
action: 'update-view',
dataset: entry.dataset,
query: entry.expectedQuery,
table: entry.table
}],
aliasStatus: 'stale',
mismatchDetails: [usesLegacySql ? 'view uses legacy SQL' : 'view query points to another target']
});
} catch (error) {
const issue = error.message;
nextEntries.push(createUnavailableReadAliasEntry(entry, issue, strict));
if (strict) {
blockingIssues.push(issue);
} else {
warnings.push(issue);
}
}
}
return createReadAliasPlanResult(nextEntries, warnings, blockingIssues);
};
export const inspectSyncManagedSchemas = async (context, runtimeConfig, options = {}, dependencies = {}) => {
const strict = options.strict === true;
const entries = createBigQueryTargetEntries(runtimeConfig, context);
if (entries.length === 0) {
return createSchemaPlanResult([], [], [], 'not-applicable');
}
let accessToken;
try {
accessToken = getBigQueryAccessToken(dependencies);
} catch (error) {
const issue = `Atlas could not resolve a Google Cloud access token for BigQuery schema inspection. ${getCommandErrorMessage(error)}`;
return createSchemaPlanResult(entries.map(entry => createUnavailableEntry(entry, issue, strict)), strict ? [] : [issue], strict ? [issue] : [], strict ? 'blocked' : 'unknown');
}
if (!accessToken) {
const issue = 'Atlas could not resolve a Google Cloud access token for BigQuery schema inspection.';
return createSchemaPlanResult(entries.map(entry => createUnavailableEntry(entry, issue, strict)), strict ? [] : [issue], strict ? [issue] : [], strict ? 'blocked' : 'unknown');
}
const nextEntries = [];
const warnings = [];
const blockingIssues = [];
for (const entry of entries) {
try {
const dataset = await readBigQueryDataset(context.projectId, entry.dataset, accessToken, dependencies);
if (!dataset) {
const datasetBlockingIssues = [];
if (!entry.datasetLocation) {
datasetBlockingIssues.push(`${createEntryLabel(entry)} cannot create missing dataset ${entry.dataset}` + 'because no dataset location could be derived from the Atlas sync config.');
}
nextEntries.push({
...entry,
actions: [{
action: 'create-dataset',
dataset: entry.dataset,
location: entry.datasetLocation,
table: entry.table,
target: entry.target
}, {
action: 'create-table',
dataset: entry.dataset,
fields: entry.nextFields,
table: entry.table,
target: entry.target
}],
blockingIssues: datasetBlockingIssues,
schemaStatus: datasetBlockingIssues.length > 0 ? 'mismatch' : 'not-found',
mismatchDetails: datasetBlockingIssues.length > 0 ? ['missing dataset location'] : ['dataset not found']
});
blockingIssues.push(...datasetBlockingIssues);
continue;
}
const remoteTable = await readBigQueryTable(context.projectId, entry.dataset, entry.table, accessToken, dependencies);
if (!remoteTable) {
nextEntries.push({
...entry,
schemaStatus: 'not-found',
mismatchDetails: ['table not found'],
actions: [{
action: 'create-table',
dataset: entry.dataset,
fields: entry.nextFields,
table: entry.table,
target: entry.target
}]
});
continue;
}
const comparison = compareBigQuerySchemas(entry.nextFields, remoteTable.schema?.fields ?? [], entry);
nextEntries.push({
...entry,
actions: comparison.actions,
blockingIssues: comparison.blockingIssues,
mismatchDetails: comparison.mismatchDetails,
schemaStatus: comparison.schemaStatus
});
blockingIssues.push(...comparison.blockingIssues);
} catch (error) {
const issue = error.message;
nextEntries.push(createUnavailableEntry(entry, issue, strict));
if (strict) {
blockingIssues.push(issue);
} else {
warnings.push(issue);
}
}
}
return createSchemaPlanResult(nextEntries, warnings, blockingIssues);
};
const applySchemaActions = async (context, entries, dependencies = {}) => {
const accessToken = getBigQueryAccessToken(dependencies);
const createdDatasets = new Set();
for (const entry of entries) {
for (const action of entry.actions) {
if (action.action === 'create-dataset') {
const datasetKey = `${context.projectId}:${action.dataset}`;
if (createdDatasets.has(datasetKey)) {
continue;
}
await createBigQueryDataset(context.projectId, action.dataset, action.location, accessToken, dependencies);
createdDatasets.add(datasetKey);
continue;
}
if (action.action === 'create-table') {
await createBigQueryTable(context.projectId, action.dataset, action.table, entry.nextFields, accessToken, dependencies);
continue;
}
if (action.action === 'add-field') {
await updateBigQueryTableSchema(context.projectId, entry.dataset, entry.table, entry.nextFields, accessToken, dependencies);
break;
}
}
}
};
export const reconcileSyncManagedSchemas = async (context, runtimeConfig, options = {}, dependencies = {}) => {
const inspection = await inspectSyncManagedSchemas(context, runtimeConfig, {
strict: options.strict !== false
}, dependencies);
if (inspection.blockingIssues.length > 0 || options.dryRun === true) {
return inspection;
}
const actionableEntries = inspection.entries.filter(entry => entry.actions.length > 0);
if (actionableEntries.length === 0) {
return inspection;
}
await applySchemaActions(context, actionableEntries, dependencies);
const finalInspection = await inspectSyncManagedSchemas(context, runtimeConfig, {
strict: options.strict !== false
}, dependencies);
return {
...finalInspection,
actions: inspection.actions,
status: finalInspection.blockingIssues.length > 0 ? 'blocked' : 'reconciled'
};
};
const applyReadAliasActions = async (context, entries, dependencies = {}) => {
const accessToken = getBigQueryAccessToken(dependencies);
const createdDatasets = new Set();
for (const entry of entries) {
for (const action of entry.actions) {
if (action.action === 'create-dataset') {
const datasetKey = `${context.projectId}:${action.dataset}`;
if (createdDatasets.has(datasetKey)) {
continue;
}
await createBigQueryDataset(context.projectId, action.dataset, action.location, accessToken, dependencies);
createdDatasets.add(datasetKey);
continue;
}
if (action.action === 'create-view') {
await createBigQueryView(context.projectId, action.dataset, action.table, action.query, accessToken, dependencies);
continue;
}
if (action.action === 'update-view') {
await updateBigQueryView(context.projectId, action.dataset, action.table, action.query, accessToken, dependencies);
}
}
}
};
export const reconcileSyncReadAliases = async (context, runtimeConfig, options = {}, dependencies = {}) => {
const inspection = await inspectSyncReadAliases(context, runtimeConfig, {
strict: options.strict !== false
}, dependencies);
if (inspection.blockingIssues.length > 0 || options.dryRun === true) {
return inspection;
}
const actionableEntries = inspection.entries.filter(entry => entry.actions.length > 0);
if (actionableEntries.length === 0) {
return inspection;
}
await applyReadAliasActions(context, actionableEntries, dependencies);
const finalInspection = await inspectSyncReadAliases(context, runtimeConfig, {
strict: options.strict !== false
}, dependencies);
return {
...finalInspection,
actions: inspection.actions,
status: finalInspection.blockingIssues.length > 0 ? 'blocked' : 'reconciled'
};
};
export const createSyncSchemaPlanSummaryRows = schemaPlan => {
if (!schemaPlan || typeof schemaPlan !== 'object') {
return [];
}
return [{
label: 'Schema planning',
value: schemaPlan.status ?? 'unknown'
}, {
label: 'Managed BigQuery targets',
value: schemaPlan.entries?.length ?? 0
}, {
label: 'Schema actions',
value: schemaPlan.actions?.length ?? 0
}, {
label: 'Schema blocking issues',
value: schemaPlan.blockingIssues?.length ?? 0
}];
};
export const createSyncReadAliasSummaryRows = readAliasPlan => {
if (!readAliasPlan || typeof readAliasPlan !== 'object') {
return [];
}
return [{
label: 'Read alias planning',
value: readAliasPlan.status ?? 'unknown'
}, {
label: 'Managed read aliases',
value: readAliasPlan.entries?.length ?? 0
}, {
label: 'Read alias actions',
value: readAliasPlan.actions?.length ?? 0
}, {
label: 'Read alias blocking issues',
value: readAliasPlan.blockingIssues?.length ?? 0
}];
};
export const createSyncSchemaPlanEntryRows = schemaPlan => (schemaPlan?.entries ?? []).map(entry => ({
label: createEntryLabel(entry),
value: entry.schemaStatus === 'match' ? 'match' : entry.schemaStatus === 'unknown' ? 'unknown' : `${entry.schemaStatus}${entry.actions.length > 0 ? ` [actions=${entry.actions.length}]` : ''}${entry.blockingIssues.length > 0 ? ` [blocking=${entry.blockingIssues.length}]` : ''}`
}));
export const createSyncReadAliasEntryRows = readAliasPlan => (readAliasPlan?.entries ?? []).map(entry => ({
label: createReadAliasEntryLabel(entry),
value: entry.aliasStatus === 'match' ? `match -> ${entry.targetDataset}.${entry.targetTable}` : entry.aliasStatus === 'unknown' ? 'unknown' : `${entry.aliasStatus} -> ${entry.targetDataset}.${entry.targetTable}${entry.actions.length > 0 ? ` [actions=${entry.actions.length}]` : ''}${entry.blockingIssues.length > 0 ? ` [blocking=${entry.blockingIssues.length}]` : ''}`
}));
export default {
createSyncReadAliasEntryRows,
createSyncReadAliasSummaryRows,
createSyncSchemaPlanEntryRows,
createSyncSchemaPlanSummaryRows,
inspectSyncManagedSchemas,
inspectSyncReadAliases,
reconcileSyncReadAliases,
reconcileSyncManagedSchemas
};