UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

710 lines 30.8 kB
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 };