#!/usr/bin/env node /** * BudgetWise – Appwrite database setup script * * Run once to create the database, collections, and attributes * in your self-hosted Appwrite instance. * * Usage: * cp .env.example .env * # Fill in VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID, APPWRITE_API_KEY * npm run setup:appwrite */ const { Client, Databases, Permission, Role, IndexType } = require('node-appwrite'); const fs = require('fs'); const path = require('path'); // ── Load .env manually (no dotenv dependency needed) ───────────────────────── function loadEnv() { const envPath = path.join(__dirname, '..', '.env'); if (!fs.existsSync(envPath)) { console.error('❌ .env file not found. Copy .env.example from the repo root to apps/appwrite/.env and fill in the values.'); process.exit(1); } const lines = fs.readFileSync(envPath, 'utf8').split('\n'); const env = {}; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const [key, ...rest] = trimmed.split('='); env[key.trim()] = rest.join('=').trim(); } return env; } const env = loadEnv(); const ENDPOINT = env.VITE_APPWRITE_ENDPOINT; const PROJECT_ID = env.VITE_APPWRITE_PROJECT_ID; const API_KEY = env.APPWRITE_API_KEY; if (!ENDPOINT || !PROJECT_ID || !API_KEY) { console.error('❌ Missing VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID, or APPWRITE_API_KEY in .env'); process.exit(1); } const client = new Client() .setEndpoint(ENDPOINT) .setProject(PROJECT_ID) .setKey(API_KEY); const databases = new Databases(client); const DB_ID = 'budget_db'; const DB_NAME = 'BudgetWise'; // ── Permissions: only authenticated users can access their own docs ────────── // We rely on document-level security with user_id filtering. const PERMISSIONS = [ Permission.read(Role.users()), Permission.create(Role.users()), Permission.update(Role.users()), Permission.delete(Role.users()), ]; async function createCollection(id, name, attrs, indexes = []) { console.log(`\n📁 Creating collection: ${name}`); try { await databases.createCollection(DB_ID, id, name, PERMISSIONS); console.log(` ✓ Collection created`); } catch (err) { if (err.code === 409) { console.log(` ⚠ Collection already exists, skipping...`); } else { throw err; } } // Add attributes for (const attr of attrs) { try { if (attr.type === 'string') { await databases.createStringAttribute(DB_ID, id, attr.key, attr.size ?? 255, attr.required ?? false, attr.default ?? null); } else if (attr.type === 'float') { await databases.createFloatAttribute(DB_ID, id, attr.key, attr.required ?? false, undefined, undefined, attr.default ?? null); } else if (attr.type === 'integer') { await databases.createIntegerAttribute(DB_ID, id, attr.key, attr.required ?? false, undefined, undefined, attr.default ?? null); } else if (attr.type === 'boolean') { await databases.createBooleanAttribute(DB_ID, id, attr.key, attr.required ?? false, attr.default ?? null); } process.stdout.write(` ✓ ${attr.key}\n`); // Wait a bit between attributes (Appwrite rate limits) await sleep(300); } catch (err) { if (err.code === 409) { process.stdout.write(` ⚠ ${attr.key} (already exists)\n`); } else { console.error(` ❌ ${attr.key}: ${err.message}`); } } } // Add indexes for (const idx of indexes) { try { await databases.createIndex(DB_ID, id, idx.key, IndexType.Key, idx.attributes, idx.orders ?? []); process.stdout.write(` ✓ index: ${idx.key}\n`); await sleep(300); } catch (err) { if (err.code === 409) { process.stdout.write(` ⚠ index ${idx.key} (already exists)\n`); } else { console.error(` ❌ index ${idx.key}: ${err.message}`); } } } } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function main() { console.log('🚀 BudgetWise Appwrite Setup'); console.log(` Endpoint : ${ENDPOINT}`); console.log(` Project : ${PROJECT_ID}`); // Create database console.log('\n🗄 Creating database...'); try { await databases.create(DB_ID, DB_NAME); console.log(' ✓ Database created'); } catch (err) { if (err.code === 409) { console.log(' ⚠ Database already exists, continuing...'); } else { throw err; } } // ── balance_sheets ──────────────────────────────────────────────────────── await createCollection( 'balance_sheets', 'Balance Sheets', [ { type: 'integer', key: 'month', required: true }, { type: 'integer', key: 'year', required: true }, { type: 'string', key: 'buffer_type', required: true, size: 20 }, { type: 'float', key: 'buffer_value', required: true, default: 0 }, { type: 'string', key: 'user_id', required: true, size: 64 }, ], [ { key: 'user_month_year', attributes: ['user_id', 'month', 'year'], orders: ['ASC', 'DESC', 'DESC'] }, ], ); // ── incomes ─────────────────────────────────────────────────────────────── await createCollection( 'incomes', 'Incomes', [ { type: 'string', key: 'balance_sheet_id', required: true, size: 64 }, { type: 'string', key: 'name', required: true, size: 128 }, { type: 'float', key: 'amount', required: true }, { type: 'string', key: 'frequency', required: true, size: 20 }, { type: 'string', key: 'user_id', required: true, size: 64 }, ], [ { key: 'user_sheet', attributes: ['user_id', 'balance_sheet_id'] }, ], ); // ── buckets ─────────────────────────────────────────────────────────────── await createCollection( 'buckets', 'Buckets', [ { type: 'string', key: 'name', required: true, size: 128 }, { type: 'string', key: 'description', required: false, size: 512, default: '' }, { type: 'string', key: 'type', required: true, size: 32 }, { type: 'float', key: 'current_balance', required: true, default: 0 }, { type: 'float', key: 'goal_amount', required: false, default: 0 }, { type: 'string', key: 'goal_type', required: false, size: 20, default: 'amount' }, { type: 'string', key: 'goal_frequency', required: false, size: 20, default: 'monthly' }, { type: 'float', key: 'return_percent', required: false, default: 0 }, { type: 'string', key: 'return_frequency', required: false, size: 20, default: 'yearly' }, { type: 'string', key: 'color', required: true, size: 16 }, { type: 'integer', key: 'sort_order', required: false, default: 0 }, { type: 'string', key: 'user_id', required: true, size: 64 }, ], [ { key: 'user_order', attributes: ['user_id', 'sort_order'], orders: ['ASC', 'ASC'] }, ], ); // ── debts ───────────────────────────────────────────────────────────────── await createCollection( 'debts', 'Debts', [ { type: 'string', key: 'name', required: true, size: 128 }, { type: 'float', key: 'principal', required: true }, { type: 'float', key: 'remaining_balance', required: true }, { type: 'float', key: 'interest_rate', required: true }, { type: 'string', key: 'interest_frequency', required: true, size: 20 }, { type: 'integer', key: 'term_months', required: true }, { type: 'float', key: 'monthly_payment', required: true }, { type: 'boolean', key: 'is_auto_calculated', required: true, default: false }, { type: 'string', key: 'start_date', required: true, size: 24 }, { type: 'string', key: 'user_id', required: true, size: 64 }, ], [ { key: 'user_id_idx', attributes: ['user_id'] }, ], ); // ── transactions ────────────────────────────────────────────────────────── await createCollection( 'transactions', 'Transactions', [ { type: 'string', key: 'bucket_id', required: true, size: 64 }, { type: 'string', key: 'type', required: true, size: 20 }, { type: 'float', key: 'amount', required: true }, { type: 'string', key: 'date', required: true, size: 24 }, { type: 'string', key: 'notes', required: false, size: 512, default: '' }, { type: 'float', key: 'balance_after', required: true }, { type: 'string', key: 'user_id', required: true, size: 64 }, ], [ { key: 'user_bucket_date', attributes: ['user_id', 'bucket_id', 'date'], orders: ['ASC', 'ASC', 'DESC'] }, ], ); console.log('\n\n✅ Setup complete!'); console.log('\nNext steps:'); console.log(' 1. npm run build'); console.log(' 2. Deploy the dist/ folder to your web server'); console.log(' 3. Open the app in Chrome on Android → "Add to Home Screen"'); } main().catch((err) => { console.error('\n❌ Setup failed:', err.message ?? err); process.exit(1); });