UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

235 lines 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.syncCommand = syncCommand; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const sheetAdapter_1 = require("../../adapter/sheetAdapter"); const oauth_1 = require("../../auth/oauth"); const schemaHash_1 = require("../../utils/schemaHash"); const actorConfig_1 = require("../../utils/actorConfig"); const cliFiles_1 = require("../../utils/cliFiles"); const backoff_1 = require("../lib/backoff"); const oauthFlow_1 = require("../lib/oauthFlow"); function loadSchemasForActor(role, schemasRoot) { const schemas = []; const actorDir = path_1.default.join(schemasRoot, role); if (!fs_1.default.existsSync(actorDir)) return schemas; const files = fs_1.default.readdirSync(actorDir).filter((f) => f.endsWith('.ts')); for (const file of files) { try { const schema = require(path_1.default.join(actorDir, file)).default; schemas.push(schema); } catch (error) { console.error(chalk_1.default.red(` ❌ Failed to load schema: ${file}${error}`)); } } return schemas; } function printStatusTable(rows) { const colW = [10, 26, 8, 12]; const header = ['Actor', 'Sheet ID', 'Tables', 'Status']; const sep = colW.map((w) => '─'.repeat(w)).join('─┼─'); const fmt = (row) => row.map((cell, i) => cell.padEnd(colW[i])).join(' │ '); console.log(); console.log(chalk_1.default.bold(fmt(header))); console.log(sep); for (const r of rows) { const sheetDisplay = r.sheetId ? r.sheetId.slice(0, 24) + (r.sheetId.length > 24 ? '..' : '') : chalk_1.default.gray('(not set)'); console.log(fmt([r.actor, sheetDisplay, String(r.tables), r.status])); } console.log(); } async function syncCommand(options) { console.log(chalk_1.default.blue.bold('🔄 Syncing schemas to Google Sheets...\n')); require('dotenv').config(); const requiredEnvVars = ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REDIRECT_URI']; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(chalk_1.default.red(`❌ Missing environment variable: ${envVar}`)); process.exit(1); } } let config; try { config = require((0, cliFiles_1.resolveConfigPath)()).default; } catch { console.error(chalk_1.default.red('❌ lsdb.config.ts not found. Run: lsdb init')); process.exit(1); } // Validate admin actor has a sheet ID set const adminActor = config.actors.find((a) => (0, actorConfig_1.resolveActorName)(a) === 'admin'); const adminSheetId = adminActor ? process.env[adminActor.sheetIdEnv] : process.env.ADMIN_SHEET_ID; if (!adminSheetId) { console.error(chalk_1.default.red(`❌ Admin sheet ID not set. Add ${adminActor?.sheetIdEnv ?? 'ADMIN_SHEET_ID'} to your .env`)); process.exit(1); } const schemasRoot = config.schemasDir ? path_1.default.resolve(process.cwd(), config.schemasDir) : path_1.default.join(process.cwd(), 'schemas'); // Collect all schemas across actors const allSchemas = []; for (const actor of config.actors) { allSchemas.push(...loadSchemasForActor((0, actorConfig_1.resolveActorName)(actor), schemasRoot)); } if (allSchemas.length === 0) { console.log(chalk_1.default.yellow('⚠️ No schemas found. Nothing to sync.')); return; } console.log(chalk_1.default.cyan(`Found ${allSchemas.length} schema(s). Starting OAuth flow...\n`)); const oauth = (0, oauth_1.createOAuthManager)({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectUri: process.env.GOOGLE_REDIRECT_URI, }); let tokens; try { if (options.tokenFile) { const tokenFilePath = path_1.default.resolve(process.cwd(), options.tokenFile); if (!fs_1.default.existsSync(tokenFilePath)) { console.error(chalk_1.default.red(`❌ Token file not found: ${tokenFilePath}`)); process.exit(1); } tokens = JSON.parse(fs_1.default.readFileSync(tokenFilePath, 'utf-8')); console.log(chalk_1.default.green(`✅ Loaded tokens from ${options.tokenFile}\n`)); } else { tokens = await (0, oauthFlow_1.resolveTokens)(oauth); } } catch (err) { console.error(chalk_1.default.red(`❌ Authentication failed: ${err}`)); process.exit(1); } const adapter = (0, sheetAdapter_1.createSheetAdapter)({ adminSheetId, credentials: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectUri: process.env.GOOGLE_REDIRECT_URI, }, tokens, }); adapter.registerSchemas(allSchemas); const statusRows = []; let totalSynced = 0; let totalFailed = 0; console.log(chalk_1.default.bold('Syncing actor schemas...\n')); for (const actorCfg of config.actors) { const actorName = (0, actorConfig_1.resolveActorName)(actorCfg); const sheetId = actorName === 'admin' ? adminSheetId : process.env[actorCfg.sheetIdEnv]; const actorSchemas = allSchemas.filter((s) => s.actor === actorName); if (!sheetId) { console.log(chalk_1.default.yellow(` ⚠ ${actorName}: ${actorCfg.sheetIdEnv} not set — skipping`)); statusRows.push({ actor: actorName, sheetId: '', tables: actorSchemas.length, status: '⚠ skipped' }); continue; } if (actorSchemas.length === 0) { console.log(chalk_1.default.gray(` - ${actorName}: no schemas`)); statusRows.push({ actor: actorName, sheetId, tables: 0, status: '– no schemas' }); continue; } let actorSynced = 0; let actorFailed = 0; const syncAdapter = actorName === 'admin' ? adapter : adapter.withContext({ userId: 'sync-cli', actor: actorName, actorSheetId: sheetId }); for (const schema of actorSchemas) { try { await syncAdapter.syncSchema(schema); actorSynced++; totalSynced++; } catch (err) { console.error(chalk_1.default.red(` ✖ ${schema.name}${err}`)); actorFailed++; totalFailed++; } } const status = actorFailed === 0 ? '✅ synced' : `❌ ${actorFailed} failed`; statusRows.push({ actor: actorName, sheetId, tables: actorSynced, status }); console.log(chalk_1.default.green(` ✓ ${actorName}: ${actorSynced} table(s) synced`)); } // --all-users: push user actor schemas to all registered user sheets if (options.allUsers) { const userSchemas = allSchemas.filter((s) => s.actor !== 'admin'); const dryRun = options.dryRun ?? false; if (dryRun) { console.log(chalk_1.default.bold.yellow('\n[DRY RUN] --all-users: previewing changes without applying...\n')); } else { console.log(chalk_1.default.bold('\nSyncing to all registered user sheets (--all-users)...\n')); } if (userSchemas.length === 0) { console.log(chalk_1.default.yellow('⚠️ No user schemas found — nothing to sync.')); } else { try { const usersTable = adapter.table('users'); const allUsers = await usersTable.findMany({}); const usersWithSheets = allUsers.filter((u) => u.actor_sheet_id); if (usersWithSheets.length === 0) { console.log(chalk_1.default.yellow('⚠️ No users with actor_sheet_id found in admin users table.')); } let allUsersSynced = 0; let allUsersFailed = 0; for (const user of usersWithSheets) { const actorSheetId = user.actor_sheet_id; const roleSchemas = userSchemas.filter((s) => s.actor === user.role); if (roleSchemas.length === 0) continue; const userAdapter = adapter.withContext({ userId: user.user_id, actor: user.role, actorSheetId, }); console.log(chalk_1.default.cyan(` ${user.email ?? user.user_id} (${user.role}) → ${actorSheetId}`)); for (const schema of roleSchemas) { const currentHash = (0, schemaHash_1.computeSchemaHash)(schema); const stored = await adapter.getSchemaVersion(actorSheetId, schema.name); const needsSync = !stored || stored.schema_hash !== currentHash; if (!needsSync) { console.log(chalk_1.default.gray(` ✓ ${schema.name} — up to date`)); continue; } if (dryRun) { const reason = !stored ? 'new table' : 'schema changed'; console.log(chalk_1.default.yellow(` ~ ${schema.name} — would sync (${reason})`)); allUsersSynced++; continue; } try { await (0, backoff_1.withBackoff)(() => userAdapter.syncSchema(schema)); await adapter.upsertSchemaVersion(actorSheetId, schema.name, currentHash, Object.keys(schema.columns).length); console.log(chalk_1.default.green(` ✓ ${schema.name} — synced`)); allUsersSynced++; totalSynced++; } catch (err) { console.error(chalk_1.default.red(` ✖ ${schema.name}${err}`)); allUsersFailed++; totalFailed++; } } } const verb = dryRun ? 'would sync' : 'synced'; console.log(); console.log(chalk_1.default.bold(`All-users result: ${allUsersSynced} ${verb}, ${allUsersFailed} failed.`)); } catch (err) { console.log(chalk_1.default.yellow(`⚠️ Could not fetch users table for --all-users sync: ${err}`)); } } } printStatusTable(statusRows); console.log(chalk_1.default.bold(`Sync complete: ${totalSynced} synced, ${totalFailed} failed.`)); if (totalFailed > 0) process.exit(1); } //# sourceMappingURL=sync.js.map