Files
budget-app/apps/appwrite/scripts/setup-appwrite.cjs
Kushal Gaywala e0e0cc65f1 chore: convert to Turborepo + npm workspaces monorepo
- Move React/Vite frontend to apps/web/ (@budgetwise/web)
- Add apps/appwrite/ (@budgetwise/appwrite) to source-control the
  Appwrite backend: declarative schema in appwrite.json (5 collections),
  CLI-based deploy.sh for containerized use, functions/ dir for future
  Appwrite Functions
- Add turbo.json for task orchestration (build, deploy, dev)
- Replace .gitlab-ci.yml with Woodpecker CI pipelines in .woodpecker/:
    web-production.yml  — push to main → build + rsync to prod
    web-staging.yml     — push to staging → build + rsync to staging
    web-preview.yml     — PR open → deploy to {pr}.{domain}; PR close → cleanup
    appwrite.yml        — schema changes in apps/appwrite/ → CLI deploy
- All secrets injected via Woodpecker CI (no committed .env files)
2026-02-28 19:16:26 +01:00

246 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});