From 46cf117bdf1373cf50d276ddfe56b87795040e2d Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 10 Jul 2025 21:44:35 +0530 Subject: [PATCH] Migrate to Drizzle kit --- MIGRATION_GUIDE.md | 92 +++ bun.lock | 63 +++ drizzle.config.ts | 16 + drizzle/0000_big_xorn.sql | 130 +++++ drizzle/meta/0000_snapshot.json | 955 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 13 + package.json | 7 + scripts/manage-db.ts | 855 ++++++---------------------- src/lib/db/index.ts | 465 +--------------- src/lib/db/schema.sql | 75 --- src/lib/db/schema.ts | 547 +++++++++++++----- src/pages/api/auth/index.ts | 12 +- 12 files changed, 1872 insertions(+), 1358 deletions(-) create mode 100644 MIGRATION_GUIDE.md create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_big_xorn.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json delete mode 100644 src/lib/db/schema.sql diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..c4b1ed7 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,92 @@ +# Drizzle Kit Migration Guide + +This project now uses Drizzle Kit for database migrations, providing better schema management and migration tracking. + +## Overview + +- **Database**: SQLite (with preparation for future PostgreSQL migration) +- **ORM**: Drizzle ORM with Drizzle Kit for migrations +- **Schema Location**: `/src/lib/db/schema.ts` +- **Migrations Folder**: `/drizzle` +- **Configuration**: `/drizzle.config.ts` + +## Available Commands + +### Database Management +- `bun run init-db` - Initialize database with all migrations +- `bun run check-db` - Check database status and recent migrations +- `bun run reset-users` - Remove all users and related data +- `bun run cleanup-db` - Remove database files + +### Drizzle Kit Commands +- `bun run db:generate` - Generate new migration files from schema changes +- `bun run db:migrate` - Apply pending migrations to database +- `bun run db:push` - Push schema changes directly (development) +- `bun run db:pull` - Pull schema from database +- `bun run db:check` - Check for migration issues +- `bun run db:studio` - Open Drizzle Studio for database browsing + +## Making Schema Changes + +1. **Update Schema**: Edit `/src/lib/db/schema.ts` +2. **Generate Migration**: Run `bun run db:generate` +3. **Review Migration**: Check the generated SQL in `/drizzle` folder +4. **Apply Migration**: Run `bun run db:migrate` or restart the application + +## Migration Process + +The application automatically runs migrations on startup: +- Checks for pending migrations +- Creates migrations table if needed +- Applies all pending migrations in order +- Tracks migration history + +## Schema Organization + +### Tables +- `users` - User authentication and accounts +- `configs` - GitHub/Gitea configurations +- `repositories` - Repository mirror tracking +- `organizations` - GitHub organizations +- `mirror_jobs` - Job tracking with resilience +- `events` - Real-time event notifications + +### Indexes +All performance-critical indexes are automatically created: +- User lookups +- Repository status queries +- Organization filtering +- Job tracking +- Event channels + +## Future PostgreSQL Migration + +The setup is designed for easy PostgreSQL migration: + +1. Update `drizzle.config.ts`: +```typescript +export default defineConfig({ + dialect: "postgresql", + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dbCredentials: { + connectionString: process.env.DATABASE_URL, + }, +}); +``` + +2. Update connection in `/src/lib/db/index.ts` +3. Generate new migrations: `bun run db:generate` +4. Apply to PostgreSQL: `bun run db:migrate` + +## Troubleshooting + +### Migration Errors +- Check `/drizzle` folder for migration files +- Verify database permissions +- Review migration SQL for conflicts + +### Schema Conflicts +- Use `bun run db:check` to identify issues +- Review generated migrations before applying +- Keep schema.ts as single source of truth \ No newline at end of file diff --git a/bun.lock b/bun.lock index 3feb12d..db69a4a 100644 --- a/bun.lock +++ b/bun.lock @@ -61,6 +61,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.6.0", + "drizzle-kit": "^0.31.4", "jsdom": "^26.1.0", "tsx": "^4.20.3", "vitest": "^3.2.4", @@ -146,6 +147,8 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="], "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], @@ -162,6 +165,10 @@ "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], @@ -630,6 +637,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], @@ -742,6 +751,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], + "drizzle-orm": ["drizzle-orm@0.44.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -770,6 +781,8 @@ "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -1324,6 +1337,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -1576,6 +1591,8 @@ "@babel/template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], @@ -1626,6 +1643,8 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], @@ -1654,6 +1673,50 @@ "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..ec27a84 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dbCredentials: { + url: "./data/gitea-mirror.db", + }, + verbose: true, + strict: true, + migrations: { + table: "__drizzle_migrations", + schema: "main", + }, +}); \ No newline at end of file diff --git a/drizzle/0000_big_xorn.sql b/drizzle/0000_big_xorn.sql new file mode 100644 index 0000000..b3c6a8c --- /dev/null +++ b/drizzle/0000_big_xorn.sql @@ -0,0 +1,130 @@ +CREATE TABLE `configs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `is_active` integer DEFAULT true NOT NULL, + `github_config` text NOT NULL, + `gitea_config` text NOT NULL, + `include` text DEFAULT '["*"]' NOT NULL, + `exclude` text DEFAULT '[]' NOT NULL, + `schedule_config` text NOT NULL, + `cleanup_config` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `events` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `channel` text NOT NULL, + `payload` text NOT NULL, + `read` integer DEFAULT false NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_events_user_channel` ON `events` (`user_id`,`channel`);--> statement-breakpoint +CREATE INDEX `idx_events_created_at` ON `events` (`created_at`);--> statement-breakpoint +CREATE INDEX `idx_events_read` ON `events` (`read`);--> statement-breakpoint +CREATE TABLE `mirror_jobs` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `repository_id` text, + `repository_name` text, + `organization_id` text, + `organization_name` text, + `details` text, + `status` text DEFAULT 'imported' NOT NULL, + `message` text NOT NULL, + `timestamp` integer DEFAULT (unixepoch()) NOT NULL, + `job_type` text DEFAULT 'mirror' NOT NULL, + `batch_id` text, + `total_items` integer, + `completed_items` integer DEFAULT 0, + `item_ids` text, + `completed_item_ids` text DEFAULT '[]', + `in_progress` integer DEFAULT false NOT NULL, + `started_at` integer, + `completed_at` integer, + `last_checkpoint` integer, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_user_id` ON `mirror_jobs` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_batch_id` ON `mirror_jobs` (`batch_id`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_in_progress` ON `mirror_jobs` (`in_progress`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_job_type` ON `mirror_jobs` (`job_type`);--> statement-breakpoint +CREATE INDEX `idx_mirror_jobs_timestamp` ON `mirror_jobs` (`timestamp`);--> statement-breakpoint +CREATE TABLE `organizations` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `config_id` text NOT NULL, + `name` text NOT NULL, + `avatar_url` text NOT NULL, + `membership_role` text DEFAULT 'member' NOT NULL, + `is_included` integer DEFAULT true NOT NULL, + `destination_org` text, + `status` text DEFAULT 'imported' NOT NULL, + `last_mirrored` integer, + `error_message` text, + `repository_count` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_organizations_user_id` ON `organizations` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_organizations_config_id` ON `organizations` (`config_id`);--> statement-breakpoint +CREATE INDEX `idx_organizations_status` ON `organizations` (`status`);--> statement-breakpoint +CREATE INDEX `idx_organizations_is_included` ON `organizations` (`is_included`);--> statement-breakpoint +CREATE TABLE `repositories` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `config_id` text NOT NULL, + `name` text NOT NULL, + `full_name` text NOT NULL, + `url` text NOT NULL, + `clone_url` text NOT NULL, + `owner` text NOT NULL, + `organization` text, + `mirrored_location` text DEFAULT '', + `is_private` integer DEFAULT false NOT NULL, + `is_fork` integer DEFAULT false NOT NULL, + `forked_from` text, + `has_issues` integer DEFAULT false NOT NULL, + `is_starred` integer DEFAULT false NOT NULL, + `is_archived` integer DEFAULT false NOT NULL, + `size` integer DEFAULT 0 NOT NULL, + `has_lfs` integer DEFAULT false NOT NULL, + `has_submodules` integer DEFAULT false NOT NULL, + `language` text, + `description` text, + `default_branch` text NOT NULL, + `visibility` text DEFAULT 'public' NOT NULL, + `status` text DEFAULT 'imported' NOT NULL, + `last_mirrored` integer, + `error_message` text, + `destination_org` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `idx_repositories_user_id` ON `repositories` (`user_id`);--> statement-breakpoint +CREATE INDEX `idx_repositories_config_id` ON `repositories` (`config_id`);--> statement-breakpoint +CREATE INDEX `idx_repositories_status` ON `repositories` (`status`);--> statement-breakpoint +CREATE INDEX `idx_repositories_owner` ON `repositories` (`owner`);--> statement-breakpoint +CREATE INDEX `idx_repositories_organization` ON `repositories` (`organization`);--> statement-breakpoint +CREATE INDEX `idx_repositories_is_fork` ON `repositories` (`is_fork`);--> statement-breakpoint +CREATE INDEX `idx_repositories_is_starred` ON `repositories` (`is_starred`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `username` text NOT NULL, + `password` text NOT NULL, + `email` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..74f1ffa --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,955 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b963d828-412d-4192-b0aa-3b13b83cfba8", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "configs": { + "name": "configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "github_config": { + "name": "github_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gitea_config": { + "name": "gitea_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "include": { + "name": "include", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"*\"]'" + }, + "exclude": { + "name": "exclude", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_config": { + "name": "cleanup_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "configs_user_id_users_id_fk": { + "name": "configs_user_id_users_id_fk", + "tableFrom": "configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_events_user_channel": { + "name": "idx_events_user_channel", + "columns": [ + "user_id", + "channel" + ], + "isUnique": false + }, + "idx_events_created_at": { + "name": "idx_events_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "idx_events_read": { + "name": "idx_events_read", + "columns": [ + "read" + ], + "isUnique": false + } + }, + "foreignKeys": { + "events_user_id_users_id_fk": { + "name": "events_user_id_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mirror_jobs": { + "name": "mirror_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mirror'" + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_items": { + "name": "completed_items", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "item_ids": { + "name": "item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_item_ids": { + "name": "completed_item_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "in_progress": { + "name": "in_progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checkpoint": { + "name": "last_checkpoint", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_mirror_jobs_user_id": { + "name": "idx_mirror_jobs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_batch_id": { + "name": "idx_mirror_jobs_batch_id", + "columns": [ + "batch_id" + ], + "isUnique": false + }, + "idx_mirror_jobs_in_progress": { + "name": "idx_mirror_jobs_in_progress", + "columns": [ + "in_progress" + ], + "isUnique": false + }, + "idx_mirror_jobs_job_type": { + "name": "idx_mirror_jobs_job_type", + "columns": [ + "job_type" + ], + "isUnique": false + }, + "idx_mirror_jobs_timestamp": { + "name": "idx_mirror_jobs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "mirror_jobs_user_id_users_id_fk": { + "name": "mirror_jobs_user_id_users_id_fk", + "tableFrom": "mirror_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "is_included": { + "name": "is_included", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_count": { + "name": "repository_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_organizations_user_id": { + "name": "idx_organizations_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_organizations_config_id": { + "name": "idx_organizations_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_organizations_status": { + "name": "idx_organizations_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_organizations_is_included": { + "name": "idx_organizations_is_included", + "columns": [ + "is_included" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organizations_user_id_users_id_fk": { + "name": "organizations_user_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organizations_config_id_configs_id_fk": { + "name": "organizations_config_id_configs_id_fk", + "tableFrom": "organizations", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories": { + "name": "repositories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clone_url": { + "name": "clone_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mirrored_location": { + "name": "mirrored_location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_fork": { + "name": "is_fork", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_issues": { + "name": "has_issues", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_starred": { + "name": "is_starred", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_lfs": { + "name": "has_lfs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "has_submodules": { + "name": "has_submodules", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'imported'" + }, + "last_mirrored": { + "name": "last_mirrored", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination_org": { + "name": "destination_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "idx_repositories_user_id": { + "name": "idx_repositories_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_repositories_config_id": { + "name": "idx_repositories_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "idx_repositories_status": { + "name": "idx_repositories_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_repositories_owner": { + "name": "idx_repositories_owner", + "columns": [ + "owner" + ], + "isUnique": false + }, + "idx_repositories_organization": { + "name": "idx_repositories_organization", + "columns": [ + "organization" + ], + "isUnique": false + }, + "idx_repositories_is_fork": { + "name": "idx_repositories_is_fork", + "columns": [ + "is_fork" + ], + "isUnique": false + }, + "idx_repositories_is_starred": { + "name": "idx_repositories_is_starred", + "columns": [ + "is_starred" + ], + "isUnique": false + } + }, + "foreignKeys": { + "repositories_user_id_users_id_fk": { + "name": "repositories_user_id_users_id_fk", + "tableFrom": "repositories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "repositories_config_id_configs_id_fk": { + "name": "repositories_config_id_configs_id_fk", + "tableFrom": "repositories", + "tableTo": "configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..423b007 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1752161775910, + "tag": "0000_big_xorn", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index bd80b70..d7251c1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,12 @@ "check-db": "bun scripts/manage-db.ts check", "fix-db": "bun scripts/manage-db.ts fix", "reset-users": "bun scripts/manage-db.ts reset-users", + "db:generate": "bun drizzle-kit generate", + "db:migrate": "bun drizzle-kit migrate", + "db:push": "bun drizzle-kit push", + "db:pull": "bun drizzle-kit pull", + "db:check": "bun drizzle-kit check", + "db:studio": "bun drizzle-kit studio", "startup-recovery": "bun scripts/startup-recovery.ts", "startup-recovery-force": "bun scripts/startup-recovery.ts --force", "test-recovery": "bun scripts/test-recovery.ts", @@ -88,6 +94,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.6.0", + "drizzle-kit": "^0.31.4", "jsdom": "^26.1.0", "tsx": "^4.20.3", "vitest": "^3.2.4" diff --git a/scripts/manage-db.ts b/scripts/manage-db.ts index f7fa1a3..ae858de 100644 --- a/scripts/manage-db.ts +++ b/scripts/manage-db.ts @@ -1,7 +1,12 @@ import fs from "fs"; import path from "path"; import { Database } from "bun:sqlite"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { v4 as uuidv4 } from "uuid"; +import { users, configs, repositories, organizations, mirrorJobs, events } from "../src/lib/db/schema"; +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; // Command line arguments const args = process.argv.slice(2); @@ -13,750 +18,222 @@ if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } -// Database paths -const rootDbFile = path.join(process.cwd(), "gitea-mirror.db"); -const rootDevDbFile = path.join(process.cwd(), "gitea-mirror-dev.db"); -const dataDbFile = path.join(dataDir, "gitea-mirror.db"); -const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db"); - // Database path - ensure we use absolute path const dbPath = path.join(dataDir, "gitea-mirror.db"); /** - * Ensure all required tables exist + * Initialize database with migrations */ -async function ensureTablesExist() { - // Create or open the database - const db = new Database(dbPath); - - const requiredTables = [ - "users", - "configs", - "repositories", - "organizations", - "mirror_jobs", - "events", - ]; - - for (const table of requiredTables) { - try { - // Check if table exists - const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get(); - - if (!result) { - console.warn(`โš ๏ธ Table '${table}' is missing. Creating it now...`); - - switch (table) { - case "users": - db.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - break; - case "configs": - db.exec(` - CREATE TABLE configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - break; - case "repositories": - db.exec(` - CREATE TABLE repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - is_archived INTEGER NOT NULL DEFAULT 0, - size INTEGER NOT NULL DEFAULT 0, - has_lfs INTEGER NOT NULL DEFAULT 0, - has_submodules INTEGER NOT NULL DEFAULT 0, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - break; - case "organizations": - db.exec(` - CREATE TABLE organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - break; - case "mirror_jobs": - db.exec(` - CREATE TABLE mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- New fields for job resilience - job_type TEXT NOT NULL DEFAULT 'mirror', - batch_id TEXT, - total_items INTEGER, - completed_items INTEGER DEFAULT 0, - item_ids TEXT, -- JSON array as text - completed_item_ids TEXT DEFAULT '[]', -- JSON array as text - in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer - started_at TIMESTAMP, - completed_at TIMESTAMP, - last_checkpoint TIMESTAMP, - - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for better performance - db.exec(` - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp); - `); - break; - case "events": - db.exec(` - CREATE TABLE events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - db.exec(` - CREATE INDEX idx_events_user_channel ON events(user_id, channel); - CREATE INDEX idx_events_created_at ON events(created_at); - CREATE INDEX idx_events_read ON events(read); - `); - break; - } - console.log(`โœ… Table '${table}' created successfully.`); - } - } catch (error) { - console.error(`โŒ Error checking table '${table}':`, error); - process.exit(1); - } +async function initDatabase() { + console.log("๐Ÿ“ฆ Initializing database..."); + + // Create an empty database file if it doesn't exist + if (!fs.existsSync(dbPath)) { + fs.writeFileSync(dbPath, ""); } - // Migration: Add cleanup_config column to existing configs table + // Create SQLite instance + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); + + // Run migrations + console.log("๐Ÿ”„ Running migrations..."); try { - const db = new Database(dbPath); - - // Check if cleanup_config column exists - const tableInfo = db.query(`PRAGMA table_info(configs)`).all(); - const hasCleanupConfig = tableInfo.some((column: any) => column.name === 'cleanup_config'); - - if (!hasCleanupConfig) { - console.log("Adding cleanup_config column to configs table..."); - - // Add the column with a default value - const defaultCleanupConfig = JSON.stringify({ - enabled: false, - retentionDays: 7, - lastRun: null, - nextRun: null, - }); - - db.exec(`ALTER TABLE configs ADD COLUMN cleanup_config TEXT NOT NULL DEFAULT '${defaultCleanupConfig}'`); - console.log("โœ… cleanup_config column added successfully."); - } + migrate(db, { migrationsFolder: "./drizzle" }); + console.log("โœ… Migrations completed successfully"); } catch (error) { - console.error("โŒ Error during cleanup_config migration:", error); - // Don't exit here as this is not critical for basic functionality + console.error("โŒ Error running migrations:", error); + throw error; } + + sqlite.close(); + console.log("โœ… Database initialized successfully"); } /** * Check database status */ async function checkDatabase() { - console.log("Checking database status..."); - - // Check for database files in the root directory (which is incorrect) - if (fs.existsSync(rootDbFile)) { - console.warn( - "โš ๏ธ WARNING: Database file found in root directory: gitea-mirror.db" - ); - console.warn("This file should be in the data directory."); - console.warn( - 'Run "bun run manage-db fix" to fix this issue or "bun run cleanup-db" to remove it.' - ); + console.log("๐Ÿ” Checking database status..."); + + if (!fs.existsSync(dbPath)) { + console.log("โŒ Database does not exist at:", dbPath); + console.log("๐Ÿ’ก Run 'bun run init-db' to create the database"); + process.exit(1); } - // Check if database files exist in the data directory (which is correct) - if (fs.existsSync(dataDbFile)) { - console.log( - "โœ… Database file found in data directory: data/gitea-mirror.db" - ); - - // Check for users - try { - const db = new Database(dbPath); - - // Check for users - const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get(); - const userCount = userCountResult?.count || 0; - - if (userCount === 0) { - console.log("โ„น๏ธ No users found in the database."); - console.log( - "When you start the application, you will be directed to the signup page" - ); - console.log("to create an initial admin account."); - } else { - console.log(`โœ… ${userCount} user(s) found in the database.`); - console.log("The application will show the login page on startup."); - } - - // Check for configurations - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount === 0) { - console.log("โ„น๏ธ No configurations found in the database."); - console.log( - "You will need to set up your GitHub and Gitea configurations after login." - ); - } else { - console.log( - `โœ… ${configCount} configuration(s) found in the database.` - ); - } - } catch (error) { - console.error("โŒ Error connecting to the database:", error); - console.warn( - 'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.' - ); - } - } else { - console.warn("โš ๏ธ WARNING: Database file not found in data directory."); - console.warn('Run "bun run manage-db init" to create it.'); - } -} - -// Database schema updates and migrations have been removed -// since the application is not used by anyone yet - -/** - * Initialize the database - */ -async function initializeDatabase() { - // Check if database already exists first - if (fs.existsSync(dataDbFile)) { - console.log("โš ๏ธ Database already exists at data/gitea-mirror.db"); - console.log( - 'If you want to recreate the database, run "bun run cleanup-db" first.' - ); - console.log( - 'Or use "bun run manage-db reset-users" to just remove users without recreating tables.' - ); - - // Check if we can connect to it - try { - const db = new Database(dbPath); - db.query(`SELECT COUNT(*) as count FROM users`).get(); - console.log("โœ… Database is valid and accessible."); - return; - } catch (error) { - console.error("โŒ Error connecting to the existing database:", error); - console.log( - "The database might be corrupted. Proceeding with reinitialization..." - ); - } - } - - console.log(`Initializing database at ${dbPath}...`); + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); try { - const db = new Database(dbPath); + // Check tables + const tables = sqlite.query( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + ).all() as Array<{name: string}>; - // Create tables if they don't exist - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); + console.log("\n๐Ÿ“Š Tables found:"); + for (const table of tables) { + const count = sqlite.query(`SELECT COUNT(*) as count FROM ${table.name}`).get() as {count: number}; + console.log(` - ${table.name}: ${count.count} records`); + } - // NOTE: We no longer create a default admin user - user will create one via signup page + // Check migrations + const migrations = sqlite.query( + "SELECT * FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 5" + ).all() as Array<{hash: string, created_at: number}>; - db.exec(` - CREATE TABLE IF NOT EXISTS configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - cleanup_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - is_archived INTEGER NOT NULL DEFAULT 0, - size INTEGER NOT NULL DEFAULT 0, - has_lfs INTEGER NOT NULL DEFAULT 0, - has_submodules INTEGER NOT NULL DEFAULT 0, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - db.exec(` - CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel); - CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); - CREATE INDEX IF NOT EXISTS idx_events_read ON events(read); - `); - - // Insert default config if none exists - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount === 0) { - // Get the first user - const firstUserResult = db.query(`SELECT id FROM users LIMIT 1`).get(); - - if (firstUserResult) { - const userId = firstUserResult.id; - const configId = uuidv4(); - const githubConfig = JSON.stringify({ - username: process.env.GITHUB_USERNAME || "", - token: process.env.GITHUB_TOKEN || "", - skipForks: false, - privateRepositories: false, - mirrorIssues: false, - mirrorStarred: true, - useSpecificUser: false, - preserveOrgStructure: true, - skipStarredIssues: false, - }); - const giteaConfig = JSON.stringify({ - url: process.env.GITEA_URL || "", - token: process.env.GITEA_TOKEN || "", - username: process.env.GITEA_USERNAME || "", - organization: "", - visibility: "public", - starredReposOrg: "github", - }); - const include = JSON.stringify(["*"]); - const exclude = JSON.stringify([]); - const scheduleConfig = JSON.stringify({ - enabled: false, - interval: 3600, - lastRun: null, - nextRun: null, - }); - const cleanupConfig = JSON.stringify({ - enabled: false, - retentionDays: 7, - lastRun: null, - nextRun: null, - }); - - const stmt = db.prepare(` - INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, cleanup_config, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - configId, - userId, - "Default Configuration", - 1, - githubConfig, - giteaConfig, - include, - exclude, - scheduleConfig, - cleanupConfig, - Date.now(), - Date.now() - ); + if (migrations.length > 0) { + console.log("\n๐Ÿ“‹ Recent migrations:"); + for (const migration of migrations) { + const date = new Date(migration.created_at); + console.log(` - ${migration.hash} (${date.toLocaleString()})`); } } - console.log("โœ… Database initialization completed successfully."); + sqlite.close(); + console.log("\nโœ… Database check complete"); } catch (error) { - console.error("โŒ Error initializing database:", error); + console.error("โŒ Error checking database:", error); + sqlite.close(); process.exit(1); } } /** - * Reset users in the database + * Reset user accounts (development only) */ async function resetUsers() { - console.log(`Resetting users in database at ${dbPath}...`); - - try { - // Check if the database exists - const doesDbExist = fs.existsSync(dbPath); - - if (!doesDbExist) { - console.log( - "โŒ Database file doesn't exist. Run 'bun run manage-db init' first to create it." - ); - return; - } - - const db = new Database(dbPath); - - // Count existing users - const userCountResult = db.query(`SELECT COUNT(*) as count FROM users`).get(); - const userCount = userCountResult?.count || 0; - - if (userCount === 0) { - console.log("โ„น๏ธ No users found in the database. Nothing to reset."); - return; - } - - // Delete all users - db.exec(`DELETE FROM users`); - console.log(`โœ… Deleted ${userCount} users from the database.`); - - // Check dependent configurations that need to be removed - const configCountResult = db.query(`SELECT COUNT(*) as count FROM configs`).get(); - const configCount = configCountResult?.count || 0; - - if (configCount > 0) { - db.exec(`DELETE FROM configs`); - console.log(`โœ… Deleted ${configCount} configurations.`); - } - - // Check for dependent repositories - const repoCountResult = db.query(`SELECT COUNT(*) as count FROM repositories`).get(); - const repoCount = repoCountResult?.count || 0; - - if (repoCount > 0) { - db.exec(`DELETE FROM repositories`); - console.log(`โœ… Deleted ${repoCount} repositories.`); - } - - // Check for dependent organizations - const orgCountResult = db.query(`SELECT COUNT(*) as count FROM organizations`).get(); - const orgCount = orgCountResult?.count || 0; - - if (orgCount > 0) { - db.exec(`DELETE FROM organizations`); - console.log(`โœ… Deleted ${orgCount} organizations.`); - } - - // Check for dependent mirror jobs - const jobCountResult = db.query(`SELECT COUNT(*) as count FROM mirror_jobs`).get(); - const jobCount = jobCountResult?.count || 0; - - if (jobCount > 0) { - db.exec(`DELETE FROM mirror_jobs`); - console.log(`โœ… Deleted ${jobCount} mirror jobs.`); - } - - console.log( - "โœ… Database has been reset. The application will now prompt for a new admin account setup on next run." - ); - } catch (error) { - console.error("โŒ Error resetting users:", error); + console.log("๐Ÿ—‘๏ธ Resetting all user accounts..."); + + if (!fs.existsSync(dbPath)) { + console.log("โŒ Database does not exist"); process.exit(1); } + + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); + + try { + // Delete all data in order of foreign key dependencies + await db.delete(events); + await db.delete(mirrorJobs); + await db.delete(repositories); + await db.delete(organizations); + await db.delete(configs); + await db.delete(users); + + console.log("โœ… All user accounts and related data have been removed"); + + sqlite.close(); + } catch (error) { + console.error("โŒ Error resetting users:", error); + sqlite.close(); + process.exit(1); + } +} + +/** + * Clean up database files + */ +async function cleanupDatabase() { + console.log("๐Ÿงน Cleaning up database files..."); + + const filesToRemove = [ + dbPath, + path.join(dataDir, "gitea-mirror-dev.db"), + path.join(process.cwd(), "gitea-mirror.db"), + path.join(process.cwd(), "gitea-mirror-dev.db"), + ]; + + for (const file of filesToRemove) { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + console.log(` - Removed: ${file}`); + } + } + + console.log("โœ… Database cleanup complete"); } /** * Fix database location issues */ -async function fixDatabaseIssues() { - console.log("Checking for database issues..."); +async function fixDatabase() { + console.log("๐Ÿ”ง Fixing database location issues..."); + + // Legacy database paths + const rootDbFile = path.join(process.cwd(), "gitea-mirror.db"); + const rootDevDbFile = path.join(process.cwd(), "gitea-mirror-dev.db"); + const dataDevDbFile = path.join(dataDir, "gitea-mirror-dev.db"); - // Check for database files in the root directory + // Check for databases in wrong locations if (fs.existsSync(rootDbFile)) { - console.log("Found database file in root directory: gitea-mirror.db"); - - // If the data directory doesn't have the file, move it there - if (!fs.existsSync(dataDbFile)) { - console.log("Moving database file to data directory..."); - fs.copyFileSync(rootDbFile, dataDbFile); - console.log("Database file moved successfully."); + console.log("๐Ÿ“ Found database in root directory"); + if (!fs.existsSync(dbPath)) { + console.log(" โ†’ Moving to data directory..."); + fs.renameSync(rootDbFile, dbPath); + console.log("โœ… Database moved successfully"); } else { - console.log( - "Database file already exists in data directory. Checking for differences..." - ); - - // Compare file sizes to see which is newer/larger - const rootStats = fs.statSync(rootDbFile); - const dataStats = fs.statSync(dataDbFile); - - if ( - rootStats.size > dataStats.size || - rootStats.mtime > dataStats.mtime - ) { - console.log( - "Root database file is newer or larger. Backing up data directory file and replacing it..." - ); - fs.copyFileSync(dataDbFile, `${dataDbFile}.backup-${Date.now()}`); - fs.copyFileSync(rootDbFile, dataDbFile); - console.log("Database file replaced successfully."); - } + console.log(" โš ๏ธ Database already exists in data directory"); + console.log(" โ†’ Keeping existing data directory database"); + fs.unlinkSync(rootDbFile); + console.log(" โ†’ Removed root directory database"); } - - // Remove the root file - console.log("Removing database file from root directory..."); - fs.unlinkSync(rootDbFile); - console.log("Root database file removed."); } - // Do the same for dev database + // Clean up dev databases if (fs.existsSync(rootDevDbFile)) { - console.log( - "Found development database file in root directory: gitea-mirror-dev.db" - ); - - // If the data directory doesn't have the file, move it there - if (!fs.existsSync(dataDevDbFile)) { - console.log("Moving development database file to data directory..."); - fs.copyFileSync(rootDevDbFile, dataDevDbFile); - console.log("Development database file moved successfully."); - } else { - console.log( - "Development database file already exists in data directory. Checking for differences..." - ); - - // Compare file sizes to see which is newer/larger - const rootStats = fs.statSync(rootDevDbFile); - const dataStats = fs.statSync(dataDevDbFile); - - if ( - rootStats.size > dataStats.size || - rootStats.mtime > dataStats.mtime - ) { - console.log( - "Root development database file is newer or larger. Backing up data directory file and replacing it..." - ); - fs.copyFileSync(dataDevDbFile, `${dataDevDbFile}.backup-${Date.now()}`); - fs.copyFileSync(rootDevDbFile, dataDevDbFile); - console.log("Development database file replaced successfully."); - } - } - - // Remove the root file - console.log("Removing development database file from root directory..."); fs.unlinkSync(rootDevDbFile); - console.log("Root development database file removed."); + console.log(" โ†’ Removed root dev database"); + } + if (fs.existsSync(dataDevDbFile)) { + fs.unlinkSync(dataDevDbFile); + console.log(" โ†’ Removed data dev database"); } - // Check if database files exist in the data directory - if (!fs.existsSync(dataDbFile)) { - console.warn( - "โš ๏ธ WARNING: Production database file not found in data directory." - ); - console.warn('Run "bun run manage-db init" to create it.'); - } else { - console.log("โœ… Production database file found in data directory."); - - // Check if we can connect to the database - try { - // Try to query the database - const db = new Database(dbPath); - db.query(`SELECT 1 FROM sqlite_master LIMIT 1`).get(); - console.log(`โœ… Successfully connected to the database.`); - } catch (error) { - console.error("โŒ Error connecting to the database:", error); - console.warn( - 'The database file might be corrupted. Consider running "bun run manage-db init" to recreate it.' - ); - } - } - - console.log("Database check completed."); + console.log("โœ… Database location fixed"); } /** - * Main function to handle the command + * Auto mode - check and initialize if needed */ -async function main() { - console.log(`Database Management Tool for Gitea Mirror`); - - // Ensure all required tables exist - console.log("Ensuring all required tables exist..."); - await ensureTablesExist(); - - switch (command) { - case "check": - await checkDatabase(); - break; - case "init": - await initializeDatabase(); - break; - case "fix": - await fixDatabaseIssues(); - break; - case "reset-users": - await resetUsers(); - break; - case "auto": - // Auto mode: check, fix, and initialize if needed - console.log("Running in auto mode: check, fix, and initialize if needed"); - await fixDatabaseIssues(); - - if (!fs.existsSync(dataDbFile)) { - await initializeDatabase(); - } else { - await checkDatabase(); - } - break; - default: - console.log(` -Available commands: - check - Check database status - init - Initialize the database (only if it doesn't exist) - fix - Fix database location issues - reset-users - Remove all users and their data - auto - Automatic mode: check, fix, and initialize if needed - -Usage: bun run manage-db [command] -`); +async function autoMode() { + if (!fs.existsSync(dbPath)) { + console.log("๐Ÿ“ฆ Database not found, initializing..."); + await initDatabase(); + } else { + console.log("โœ… Database already exists"); + await checkDatabase(); } } -main().catch((error) => { - console.error("Error during database management:", error); - process.exit(1); -}); +// Execute command +switch (command) { + case "init": + await initDatabase(); + break; + case "check": + await checkDatabase(); + break; + case "fix": + await fixDatabase(); + break; + case "reset-users": + await resetUsers(); + break; + case "cleanup": + await cleanupDatabase(); + break; + case "auto": + await autoMode(); + break; + default: + console.log("Available commands:"); + console.log(" init - Initialize database with migrations"); + console.log(" check - Check database status"); + console.log(" fix - Fix database location issues"); + console.log(" reset-users - Remove all users and related data"); + console.log(" cleanup - Remove all database files"); + console.log(" auto - Auto initialize if needed"); + process.exit(1); +} \ No newline at end of file diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 66414b3..7b62d25 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1,10 +1,8 @@ -import { z } from "zod"; -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; import fs from "fs"; import path from "path"; -import { configSchema } from "./schema"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; // Define the database URL - for development we'll use a local SQLite file const dataDir = path.join(process.cwd(), "data"); @@ -26,464 +24,41 @@ try { sqlite = new Database(dbPath); console.log("Successfully connected to SQLite database using Bun's native driver"); - // Ensure all required tables exist - ensureTablesExist(sqlite); - - // Run migrations - runMigrations(sqlite); + // Run Drizzle migrations if needed + runDrizzleMigrations(); } catch (error) { console.error("Error opening database:", error); throw error; } /** - * Run database migrations + * Run Drizzle migrations */ -function runMigrations(db: Database) { +function runDrizzleMigrations() { try { - // Migration 1: Add destination_org column to organizations table - const orgTableInfo = db.query("PRAGMA table_info(organizations)").all() as Array<{name: string}>; - const hasDestinationOrg = orgTableInfo.some(col => col.name === 'destination_org'); + console.log("๐Ÿ”„ Checking for pending migrations..."); + + // Check if migrations table exists + const migrationsTableExists = sqlite + .query("SELECT name FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'") + .get(); - if (!hasDestinationOrg) { - console.log("๐Ÿ”„ Running migration: Adding destination_org column to organizations table"); - db.exec("ALTER TABLE organizations ADD COLUMN destination_org TEXT"); - console.log("โœ… Migration completed: destination_org column added"); + if (!migrationsTableExists) { + console.log("๐Ÿ“ฆ First time setup - running initial migrations..."); } - // Migration 2: Add destination_org column to repositories table - const repoTableInfo = db.query("PRAGMA table_info(repositories)").all() as Array<{name: string}>; - const hasRepoDestinationOrg = repoTableInfo.some(col => col.name === 'destination_org'); - - if (!hasRepoDestinationOrg) { - console.log("๐Ÿ”„ Running migration: Adding destination_org column to repositories table"); - db.exec("ALTER TABLE repositories ADD COLUMN destination_org TEXT"); - console.log("โœ… Migration completed: destination_org column added to repositories"); - } + // Run migrations using Drizzle migrate function + migrate(db, { migrationsFolder: "./drizzle" }); + + console.log("โœ… Database migrations completed successfully"); } catch (error) { console.error("โŒ Error running migrations:", error); - // Don't throw - migrations should be non-breaking - } -} - -/** - * Ensure all required tables exist in the database - */ -function ensureTablesExist(db: Database) { - const requiredTables = [ - "users", - "configs", - "repositories", - "organizations", - "mirror_jobs", - "events", - ]; - - for (const table of requiredTables) { - try { - // Check if table exists - const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get(); - - if (!result) { - console.warn(`โš ๏ธ Table '${table}' is missing. Creating it now...`); - createTable(db, table); - console.log(`โœ… Table '${table}' created successfully`); - } - } catch (error) { - console.error(`โŒ Error checking/creating table '${table}':`, error); - throw error; - } - } -} - -/** - * Create a specific table with its schema - */ -function createTable(db: Database, tableName: string) { - switch (tableName) { - case "users": - db.exec(` - CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - break; - - case "configs": - db.exec(` - CREATE TABLE configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active INTEGER NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - include TEXT NOT NULL DEFAULT '["*"]', - exclude TEXT NOT NULL DEFAULT '[]', - schedule_config TEXT NOT NULL, - cleanup_config TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - break; - - case "repositories": - db.exec(` - CREATE TABLE repositories ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - clone_url TEXT NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - is_private INTEGER NOT NULL DEFAULT 0, - is_fork INTEGER NOT NULL DEFAULT 0, - forked_from TEXT, - has_issues INTEGER NOT NULL DEFAULT 0, - is_starred INTEGER NOT NULL DEFAULT 0, - language TEXT, - description TEXT, - default_branch TEXT NOT NULL, - visibility TEXT NOT NULL DEFAULT 'public', - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - // Create indexes for repositories - db.exec(` - CREATE INDEX IF NOT EXISTS idx_repositories_user_id ON repositories(user_id); - CREATE INDEX IF NOT EXISTS idx_repositories_config_id ON repositories(config_id); - CREATE INDEX IF NOT EXISTS idx_repositories_status ON repositories(status); - CREATE INDEX IF NOT EXISTS idx_repositories_owner ON repositories(owner); - CREATE INDEX IF NOT EXISTS idx_repositories_organization ON repositories(organization); - CREATE INDEX IF NOT EXISTS idx_repositories_is_fork ON repositories(is_fork); - CREATE INDEX IF NOT EXISTS idx_repositories_is_starred ON repositories(is_starred); - `); - break; - - case "organizations": - db.exec(` - CREATE TABLE organizations ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - avatar_url TEXT NOT NULL, - membership_role TEXT NOT NULL DEFAULT 'member', - is_included INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'imported', - last_mirrored INTEGER, - error_message TEXT, - repository_count INTEGER NOT NULL DEFAULT 0, - destination_org TEXT, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (config_id) REFERENCES configs(id) - ) - `); - - // Create indexes for organizations - db.exec(` - CREATE INDEX IF NOT EXISTS idx_organizations_user_id ON organizations(user_id); - CREATE INDEX IF NOT EXISTS idx_organizations_config_id ON organizations(config_id); - CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status); - CREATE INDEX IF NOT EXISTS idx_organizations_is_included ON organizations(is_included); - `); - break; - - case "mirror_jobs": - db.exec(` - CREATE TABLE mirror_jobs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - repository_id TEXT, - repository_name TEXT, - organization_id TEXT, - organization_name TEXT, - details TEXT, - status TEXT NOT NULL DEFAULT 'imported', - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- New fields for job resilience - job_type TEXT NOT NULL DEFAULT 'mirror', - batch_id TEXT, - total_items INTEGER, - completed_items INTEGER DEFAULT 0, - item_ids TEXT, -- JSON array as text - completed_item_ids TEXT DEFAULT '[]', -- JSON array as text - in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer - started_at TIMESTAMP, - completed_at TIMESTAMP, - last_checkpoint TIMESTAMP, - - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for mirror_jobs - db.exec(` - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type); - CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp); - `); - break; - - case "events": - db.exec(` - CREATE TABLE events ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - read INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) - ) - `); - - // Create indexes for events - db.exec(` - CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel); - CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at); - CREATE INDEX IF NOT EXISTS idx_events_read ON events(read); - `); - break; - - default: - throw new Error(`Unknown table: ${tableName}`); + throw error; } } // Create drizzle instance with the SQLite client export const db = drizzle({ client: sqlite }); -// Simple async wrapper around SQLite API for compatibility -// This maintains backward compatibility with existing code -export const client = { - async execute(sql: string, params?: any[]) { - try { - const stmt = sqlite.query(sql); - if (/^\s*select/i.test(sql)) { - const rows = stmt.all(params ?? []); - return { rows } as { rows: any[] }; - } - stmt.run(params ?? []); - return { rows: [] } as { rows: any[] }; - } catch (error) { - console.error(`Error executing SQL: ${sql}`, error); - throw error; - } - }, -}; - -// Define the tables -export const users = sqliteTable("users", { - id: text("id").primaryKey(), - username: text("username").notNull(), - password: text("password").notNull(), - email: text("email").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -// New table for event notifications (replacing Redis pub/sub) -export const events = sqliteTable("events", { - id: text("id").primaryKey(), - userId: text("user_id").notNull().references(() => users.id), - channel: text("channel").notNull(), - payload: text("payload", { mode: "json" }).notNull(), - read: integer("read", { mode: "boolean" }).notNull().default(false), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -const githubSchema = configSchema.shape.githubConfig; -const giteaSchema = configSchema.shape.giteaConfig; -const scheduleSchema = configSchema.shape.scheduleConfig; -const cleanupSchema = configSchema.shape.cleanupConfig; - -export const configs = sqliteTable("configs", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - name: text("name").notNull(), - isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), - - githubConfig: text("github_config", { mode: "json" }) - .$type>() - .notNull(), - - giteaConfig: text("gitea_config", { mode: "json" }) - .$type>() - .notNull(), - - include: text("include", { mode: "json" }) - .$type() - .notNull() - .default(["*"]), - - exclude: text("exclude", { mode: "json" }) - .$type() - .notNull() - .default([]), - - scheduleConfig: text("schedule_config", { mode: "json" }) - .$type>() - .notNull(), - - cleanupConfig: text("cleanup_config", { mode: "json" }) - .$type>() - .notNull(), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -export const repositories = sqliteTable("repositories", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - configId: text("config_id") - .notNull() - .references(() => configs.id), - name: text("name").notNull(), - fullName: text("full_name").notNull(), - url: text("url").notNull(), - cloneUrl: text("clone_url").notNull(), - owner: text("owner").notNull(), - organization: text("organization"), - mirroredLocation: text("mirrored_location").default(""), - - isPrivate: integer("is_private", { mode: "boolean" }) - .notNull() - .default(false), - isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), - forkedFrom: text("forked_from"), - - hasIssues: integer("has_issues", { mode: "boolean" }) - .notNull() - .default(false), - isStarred: integer("is_starred", { mode: "boolean" }) - .notNull() - .default(false), - isArchived: integer("is_archived", { mode: "boolean" }) - .notNull() - .default(false), - - size: integer("size").notNull().default(0), - hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), - hasSubmodules: integer("has_submodules", { mode: "boolean" }) - .notNull() - .default(false), - - defaultBranch: text("default_branch").notNull(), - visibility: text("visibility").notNull().default("public"), - - status: text("status").notNull().default("imported"), - lastMirrored: integer("last_mirrored", { mode: "timestamp" }), - errorMessage: text("error_message"), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); - -export const mirrorJobs = sqliteTable("mirror_jobs", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - repositoryId: text("repository_id"), - repositoryName: text("repository_name"), - organizationId: text("organization_id"), - organizationName: text("organization_name"), - details: text("details"), - status: text("status").notNull().default("imported"), - message: text("message").notNull(), - timestamp: integer("timestamp", { mode: "timestamp" }) - .notNull() - .default(new Date()), - - // New fields for job resilience - jobType: text("job_type").notNull().default("mirror"), - batchId: text("batch_id"), - totalItems: integer("total_items"), - completedItems: integer("completed_items").default(0), - itemIds: text("item_ids", { mode: "json" }).$type(), - completedItemIds: text("completed_item_ids", { mode: "json" }).$type().default([]), - inProgress: integer("in_progress", { mode: "boolean" }).notNull().default(false), - startedAt: integer("started_at", { mode: "timestamp" }), - completedAt: integer("completed_at", { mode: "timestamp" }), - lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }), -}); - -export const organizations = sqliteTable("organizations", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id), - configId: text("config_id") - .notNull() - .references(() => configs.id), - name: text("name").notNull(), - - avatarUrl: text("avatar_url").notNull(), - - membershipRole: text("membership_role").notNull().default("member"), - - isIncluded: integer("is_included", { mode: "boolean" }) - .notNull() - .default(true), - - // Override destination organization for this GitHub org's repos - destinationOrg: text("destination_org"), - - status: text("status").notNull().default("imported"), - lastMirrored: integer("last_mirrored", { mode: "timestamp" }), - errorMessage: text("error_message"), - - repositoryCount: integer("repository_count").notNull().default(0), - - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .default(new Date()), -}); +// Export all table definitions from schema +export { users, events, configs, repositories, mirrorJobs, organizations } from "./schema"; diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql deleted file mode 100644 index 264645b..0000000 --- a/src/lib/db/schema.sql +++ /dev/null @@ -1,75 +0,0 @@ --- Users table -CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - email TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL -); - --- Configurations table -CREATE TABLE IF NOT EXISTS configs ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT 1, - github_config TEXT NOT NULL, - gitea_config TEXT NOT NULL, - schedule_config TEXT NOT NULL, - include TEXT NOT NULL, - exclude TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -); - --- Repositories table -CREATE TABLE IF NOT EXISTS repositories ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - full_name TEXT NOT NULL, - url TEXT NOT NULL, - is_private BOOLEAN NOT NULL, - is_fork BOOLEAN NOT NULL, - owner TEXT NOT NULL, - organization TEXT, - mirrored_location TEXT DEFAULT '', - has_issues BOOLEAN NOT NULL, - is_starred BOOLEAN NOT NULL, - status TEXT NOT NULL, - error_message TEXT, - last_mirrored DATETIME, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE -); - --- Organizations table -CREATE TABLE IF NOT EXISTS organizations ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL, - is_included BOOLEAN NOT NULL, - repository_count INTEGER NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE -); - --- Mirror jobs table -CREATE TABLE IF NOT EXISTS mirror_jobs ( - id TEXT PRIMARY KEY, - config_id TEXT NOT NULL, - repository_id TEXT, - status TEXT NOT NULL, - started_at DATETIME NOT NULL, - completed_at DATETIME, - log TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE, - FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE SET NULL -); diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index b9decb5..12ca324 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,182 +1,443 @@ import { z } from "zod"; -import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; -import { membershipRoleEnum } from "@/types/organizations"; +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; -// User schema +// ===== Zod Validation Schemas ===== export const userSchema = z.object({ - id: z.string().uuid().optional(), - username: z.string().min(3), - password: z.string().min(8).optional(), // Hashed password + id: z.string(), + username: z.string(), + password: z.string(), email: z.string().email(), - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type User = z.infer; +export const githubConfigSchema = z.object({ + owner: z.string(), + type: z.enum(["personal", "organization"]), + token: z.string(), + includeStarred: z.boolean().default(false), + includeForks: z.boolean().default(true), + includeArchived: z.boolean().default(false), + includePrivate: z.boolean().default(true), + includePublic: z.boolean().default(true), + includeOrganizations: z.array(z.string()).default([]), + starredReposOrg: z.string().optional(), + mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).default("preserve"), + defaultOrg: z.string().optional(), +}); + +export const giteaConfigSchema = z.object({ + url: z.string().url(), + token: z.string(), + defaultOwner: z.string(), + mirrorInterval: z.string().default("8h"), + lfs: z.boolean().default(false), + wiki: z.boolean().default(false), + visibility: z + .enum(["public", "private", "limited", "default"]) + .default("default"), + createOrg: z.boolean().default(true), + templateOwner: z.string().optional(), + templateRepo: z.string().optional(), + addTopics: z.boolean().default(true), + topicPrefix: z.string().optional(), + preserveVisibility: z.boolean().default(true), + forkStrategy: z + .enum(["skip", "reference", "full-copy"]) + .default("reference"), +}); + +export const scheduleConfigSchema = z.object({ + enabled: z.boolean().default(false), + interval: z.string().default("0 2 * * *"), + concurrent: z.boolean().default(false), + batchSize: z.number().default(10), + pauseBetweenBatches: z.number().default(5000), + retryAttempts: z.number().default(3), + retryDelay: z.number().default(60000), + timeout: z.number().default(3600000), + autoRetry: z.boolean().default(true), + cleanupBeforeMirror: z.boolean().default(false), + notifyOnFailure: z.boolean().default(true), + notifyOnSuccess: z.boolean().default(false), + logLevel: z.enum(["error", "warn", "info", "debug"]).default("info"), + timezone: z.string().default("UTC"), + onlyMirrorUpdated: z.boolean().default(false), + updateInterval: z.number().default(86400000), + skipRecentlyMirrored: z.boolean().default(true), + recentThreshold: z.number().default(3600000), +}); + +export const cleanupConfigSchema = z.object({ + enabled: z.boolean().default(false), + deleteFromGitea: z.boolean().default(false), + deleteIfNotInGitHub: z.boolean().default(true), + protectedRepos: z.array(z.string()).default([]), + dryRun: z.boolean().default(true), + orphanedRepoAction: z + .enum(["skip", "archive", "delete"]) + .default("archive"), + batchSize: z.number().default(10), + pauseBetweenDeletes: z.number().default(2000), +}); -// Configuration schema export const configSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid(), - name: z.string().min(1), + id: z.string(), + userId: z.string(), + name: z.string(), isActive: z.boolean().default(true), - githubConfig: z.object({ - username: z.string().min(1), - token: z.string().optional(), - skipForks: z.boolean().default(false), - privateRepositories: z.boolean().default(false), - mirrorIssues: z.boolean().default(false), - mirrorWiki: z.boolean().default(false), - mirrorStarred: z.boolean().default(false), - useSpecificUser: z.boolean().default(false), - singleRepo: z.string().optional(), - includeOrgs: z.array(z.string()).default([]), - excludeOrgs: z.array(z.string()).default([]), - mirrorPublicOrgs: z.boolean().default(false), - publicOrgs: z.array(z.string()).default([]), - skipStarredIssues: z.boolean().default(false), - }), - giteaConfig: z.object({ - username: z.string().min(1), - url: z.string().url(), - token: z.string().min(1), - organization: z.string().optional(), - visibility: z.enum(["public", "private", "limited"]).default("public"), - starredReposOrg: z.string().default("github"), - preserveOrgStructure: z.boolean().default(false), - mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).optional(), - personalReposOrg: z.string().optional(), // Override destination for personal repos - }), + githubConfig: githubConfigSchema, + giteaConfig: giteaConfigSchema, include: z.array(z.string()).default(["*"]), exclude: z.array(z.string()).default([]), - scheduleConfig: z.object({ - enabled: z.boolean().default(false), - interval: z.number().min(1).default(3600), // in seconds - lastRun: z.date().optional(), - nextRun: z.date().optional(), - }), - cleanupConfig: z.object({ - enabled: z.boolean().default(false), - retentionDays: z.number().min(1).default(604800), // in seconds (default: 7 days) - lastRun: z.date().optional(), - nextRun: z.date().optional(), - }), - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + scheduleConfig: scheduleConfigSchema, + cleanupConfig: cleanupConfigSchema, + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Config = z.infer; - -// Repository schema export const repositorySchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - configId: z.string().uuid(), - - name: z.string().min(1), - fullName: z.string().min(1), + id: z.string(), + userId: z.string(), + configId: z.string(), + name: z.string(), + fullName: z.string(), url: z.string().url(), cloneUrl: z.string().url(), - - owner: z.string().min(1), - organization: z.string().optional(), - + owner: z.string(), + organization: z.string().optional().nullable(), + mirroredLocation: z.string().default(""), isPrivate: z.boolean().default(false), isForked: z.boolean().default(false), - forkedFrom: z.string().optional(), - + forkedFrom: z.string().optional().nullable(), hasIssues: z.boolean().default(false), isStarred: z.boolean().default(false), isArchived: z.boolean().default(false), - - size: z.number(), + size: z.number().default(0), hasLFS: z.boolean().default(false), hasSubmodules: z.boolean().default(false), - + language: z.string().optional().nullable(), + description: z.string().optional().nullable(), defaultBranch: z.string(), - visibility: repositoryVisibilityEnum.default("public"), - - status: repoStatusEnum.default("imported"), - lastMirrored: z.date().optional(), - errorMessage: z.string().optional(), - - mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored - destinationOrg: z.string().optional(), // Custom destination organization override - - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + visibility: z.enum(["public", "private", "internal"]).default("public"), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + ]) + .default("imported"), + lastMirrored: z.coerce.date().optional().nullable(), + errorMessage: z.string().optional().nullable(), + destinationOrg: z.string().optional().nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Repository = z.infer; - -// Mirror job schema export const mirrorJobSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - repositoryId: z.string().uuid().optional(), - repositoryName: z.string().optional(), - organizationId: z.string().uuid().optional(), - organizationName: z.string().optional(), - details: z.string().optional(), - status: repoStatusEnum.default("imported"), + id: z.string(), + userId: z.string(), + repositoryId: z.string().optional().nullable(), + repositoryName: z.string().optional().nullable(), + organizationId: z.string().optional().nullable(), + organizationName: z.string().optional().nullable(), + details: z.string().optional().nullable(), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + ]) + .default("imported"), message: z.string(), - timestamp: z.date().default(() => new Date()), - - // New fields for job resilience - jobType: z.enum(["mirror", "sync", "retry"]).default("mirror"), - batchId: z.string().uuid().optional(), // Group related jobs together - totalItems: z.number().optional(), // Total number of items to process - completedItems: z.number().optional(), // Number of items completed - itemIds: z.array(z.string()).optional(), // IDs of items to process - completedItemIds: z.array(z.string()).optional(), // IDs of completed items - inProgress: z.boolean().default(false), // Whether the job is currently running - startedAt: z.date().optional(), // When the job started - completedAt: z.date().optional(), // When the job completed - lastCheckpoint: z.date().optional(), // Last time progress was saved + timestamp: z.coerce.date(), + jobType: z.enum(["mirror", "cleanup", "import"]).default("mirror"), + batchId: z.string().optional().nullable(), + totalItems: z.number().optional().nullable(), + completedItems: z.number().default(0), + itemIds: z.array(z.string()).optional().nullable(), + completedItemIds: z.array(z.string()).default([]), + inProgress: z.boolean().default(false), + startedAt: z.coerce.date().optional().nullable(), + completedAt: z.coerce.date().optional().nullable(), + lastCheckpoint: z.coerce.date().optional().nullable(), }); -export type MirrorJob = z.infer; - -// Organization schema export const organizationSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid().optional(), - configId: z.string().uuid(), - - avatarUrl: z.string().url(), - - name: z.string().min(1), - - membershipRole: membershipRoleEnum.default("member"), - - isIncluded: z.boolean().default(false), - - status: repoStatusEnum.default("imported"), - lastMirrored: z.date().optional(), - errorMessage: z.string().optional(), - + id: z.string(), + userId: z.string(), + configId: z.string(), + name: z.string(), + avatarUrl: z.string(), + membershipRole: z.enum(["admin", "member", "owner"]).default("member"), + isIncluded: z.boolean().default(true), + destinationOrg: z.string().optional().nullable(), + status: z + .enum([ + "imported", + "mirroring", + "mirrored", + "failed", + "skipped", + "deleting", + "deleted", + ]) + .default("imported"), + lastMirrored: z.coerce.date().optional().nullable(), + errorMessage: z.string().optional().nullable(), repositoryCount: z.number().default(0), - publicRepositoryCount: z.number().optional(), - privateRepositoryCount: z.number().optional(), - forkRepositoryCount: z.number().optional(), - - // Override destination organization for this GitHub org's repos - destinationOrg: z.string().optional(), - - createdAt: z.date().default(() => new Date()), - updatedAt: z.date().default(() => new Date()), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), }); -export type Organization = z.infer; - -// Event schema (for SQLite-based pub/sub) export const eventSchema = z.object({ - id: z.string().uuid().optional(), - userId: z.string().uuid(), - channel: z.string().min(1), + id: z.string(), + userId: z.string(), + channel: z.string(), payload: z.any(), read: z.boolean().default(false), - createdAt: z.date().default(() => new Date()), + createdAt: z.coerce.date(), }); -export type Event = z.infer; +// ===== Drizzle Table Definitions ===== + +export const users = sqliteTable("users", { + id: text("id").primaryKey(), + username: text("username").notNull(), + password: text("password").notNull(), + email: text("email").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}); + +export const events = sqliteTable("events", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + channel: text("channel").notNull(), + payload: text("payload", { mode: "json" }).notNull(), + read: integer("read", { mode: "boolean" }).notNull().default(false), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userChannelIdx: index("idx_events_user_channel").on(table.userId, table.channel), + createdAtIdx: index("idx_events_created_at").on(table.createdAt), + readIdx: index("idx_events_read").on(table.read), + }; +}); + +export const configs = sqliteTable("configs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + name: text("name").notNull(), + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), + + githubConfig: text("github_config", { mode: "json" }) + .$type>() + .notNull(), + + giteaConfig: text("gitea_config", { mode: "json" }) + .$type>() + .notNull(), + + include: text("include", { mode: "json" }) + .$type() + .notNull() + .default(sql`'["*"]'`), + + exclude: text("exclude", { mode: "json" }) + .$type() + .notNull() + .default(sql`'[]'`), + + scheduleConfig: text("schedule_config", { mode: "json" }) + .$type>() + .notNull(), + + cleanupConfig: text("cleanup_config", { mode: "json" }) + .$type>() + .notNull(), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}); + +export const repositories = sqliteTable("repositories", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + fullName: text("full_name").notNull(), + url: text("url").notNull(), + cloneUrl: text("clone_url").notNull(), + owner: text("owner").notNull(), + organization: text("organization"), + mirroredLocation: text("mirrored_location").default(""), + + isPrivate: integer("is_private", { mode: "boolean" }) + .notNull() + .default(false), + isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), + forkedFrom: text("forked_from"), + + hasIssues: integer("has_issues", { mode: "boolean" }) + .notNull() + .default(false), + isStarred: integer("is_starred", { mode: "boolean" }) + .notNull() + .default(false), + isArchived: integer("is_archived", { mode: "boolean" }) + .notNull() + .default(false), + + size: integer("size").notNull().default(0), + hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), + hasSubmodules: integer("has_submodules", { mode: "boolean" }) + .notNull() + .default(false), + + language: text("language"), + description: text("description"), + defaultBranch: text("default_branch").notNull(), + visibility: text("visibility").notNull().default("public"), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + destinationOrg: text("destination_org"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_repositories_user_id").on(table.userId), + configIdIdx: index("idx_repositories_config_id").on(table.configId), + statusIdx: index("idx_repositories_status").on(table.status), + ownerIdx: index("idx_repositories_owner").on(table.owner), + organizationIdx: index("idx_repositories_organization").on(table.organization), + isForkedIdx: index("idx_repositories_is_fork").on(table.isForked), + isStarredIdx: index("idx_repositories_is_starred").on(table.isStarred), + }; +}); + +export const mirrorJobs = sqliteTable("mirror_jobs", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + repositoryId: text("repository_id"), + repositoryName: text("repository_name"), + organizationId: text("organization_id"), + organizationName: text("organization_name"), + details: text("details"), + status: text("status").notNull().default("imported"), + message: text("message").notNull(), + timestamp: integer("timestamp", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + + // Job resilience fields + jobType: text("job_type").notNull().default("mirror"), + batchId: text("batch_id"), + totalItems: integer("total_items"), + completedItems: integer("completed_items").default(0), + itemIds: text("item_ids", { mode: "json" }).$type(), + completedItemIds: text("completed_item_ids", { mode: "json" }) + .$type() + .default(sql`'[]'`), + inProgress: integer("in_progress", { mode: "boolean" }) + .notNull() + .default(false), + startedAt: integer("started_at", { mode: "timestamp" }), + completedAt: integer("completed_at", { mode: "timestamp" }), + lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }), +}, (table) => { + return { + userIdIdx: index("idx_mirror_jobs_user_id").on(table.userId), + batchIdIdx: index("idx_mirror_jobs_batch_id").on(table.batchId), + inProgressIdx: index("idx_mirror_jobs_in_progress").on(table.inProgress), + jobTypeIdx: index("idx_mirror_jobs_job_type").on(table.jobType), + timestampIdx: index("idx_mirror_jobs_timestamp").on(table.timestamp), + }; +}); + +export const organizations = sqliteTable("organizations", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + configId: text("config_id") + .notNull() + .references(() => configs.id), + name: text("name").notNull(), + + avatarUrl: text("avatar_url").notNull(), + + membershipRole: text("membership_role").notNull().default("member"), + + isIncluded: integer("is_included", { mode: "boolean" }) + .notNull() + .default(true), + + destinationOrg: text("destination_org"), + + status: text("status").notNull().default("imported"), + lastMirrored: integer("last_mirrored", { mode: "timestamp" }), + errorMessage: text("error_message"), + + repositoryCount: integer("repository_count").notNull().default(0), + + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}, (table) => { + return { + userIdIdx: index("idx_organizations_user_id").on(table.userId), + configIdIdx: index("idx_organizations_config_id").on(table.configId), + statusIdx: index("idx_organizations_status").on(table.status), + isIncludedIdx: index("idx_organizations_is_included").on(table.isIncluded), + }; +}); + +// Export type definitions +export type User = z.infer; +export type Config = z.infer; +export type Repository = z.infer; +export type MirrorJob = z.infer; +export type Organization = z.infer; +export type Event = z.infer; \ No newline at end of file diff --git a/src/pages/api/auth/index.ts b/src/pages/api/auth/index.ts index 1c2f936..8eeb62b 100644 --- a/src/pages/api/auth/index.ts +++ b/src/pages/api/auth/index.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; -import { db, users, configs, client } from "@/lib/db"; -import { eq, and } from "drizzle-orm"; +import { db, users, configs } from "@/lib/db"; +import { eq, and, sql } from "drizzle-orm"; import jwt from "jsonwebtoken"; const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; @@ -10,10 +10,10 @@ export const GET: APIRoute = async ({ request, cookies }) => { const token = authHeader?.split(" ")[1] || cookies.get("token")?.value; if (!token) { - const userCountResult = await client.execute( - `SELECT COUNT(*) as count FROM users` - ); - const userCount = userCountResult.rows[0].count; + const userCountResult = await db + .select({ count: sql`count(*)` }) + .from(users); + const userCount = userCountResult[0].count; if (userCount === 0) { return new Response(JSON.stringify({ error: "No users found" }), {