longcelot-sheet-db
Version:
Google Sheets-backed staging database adapter for Node.js with schema-first design
235 lines • 11.2 kB
JavaScript
;
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