diff --git a/.env.example b/.env.example index 5fa2ae8..33e29ab 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,15 @@ # App HOST= PORT= +JWT_SECRET=supersecretkey # Database -DATABASE_PATH= +DATABASE_PATH=postgres://postgres:postgres@127.0.0.1:5432/github_leaderboard -# Auth +# Github API GITHUB_CLIENT_ID= GITHUB_SECRET_KEY= -JWT_SECRET=supersecretkey +GITHUB_DEFAULT_TOKEN= # Queue Worker QUEUE_CONCURRENCY=1 diff --git a/package.json b/package.json index 1ac4e7f..d1a5cee 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "drizzle-orm": "^0.32.2", "hono": "^4.5.4", "pino": "^9.3.2", + "postgres": "^3.4.4", "react": "^18.3.1", "react-daisyui": "^5.0.3", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d71832..ad53c91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,13 +25,16 @@ importers: version: 1.11.12 drizzle-orm: specifier: ^0.32.2 - version: 0.32.2(@types/react@18.3.3)(better-sqlite3@11.1.2)(bun-types@1.1.17)(react@18.3.1) + version: 0.32.2(@types/pg@8.11.6)(@types/react@18.3.3)(better-sqlite3@11.1.2)(bun-types@1.1.17)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1) hono: specifier: ^4.5.4 version: 4.5.4 pino: specifier: ^9.3.2 version: 9.3.2 + postgres: + specifier: ^3.4.4 + version: 3.4.4 react: specifier: ^18.3.1 version: 18.3.1 @@ -987,6 +990,9 @@ packages: '@types/node@20.12.14': resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} + '@types/pg@8.11.6': + resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -1908,6 +1914,9 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -1963,6 +1972,48 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.6.4: + resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.6.2: + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.12.0: + resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -2033,6 +2084,45 @@ packages: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + + postgres@3.4.4: + resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==} + engines: {node: '>=12'} + prebuild-install@7.1.2: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} @@ -2484,6 +2574,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3089,6 +3183,13 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/pg@8.11.6': + dependencies: + '@types/node': 20.12.14 + pg-protocol: 1.6.1 + pg-types: 4.0.2 + optional: true + '@types/prop-types@15.7.12': {} '@types/react-dom@18.3.0': @@ -3516,11 +3617,14 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.32.2(@types/react@18.3.3)(better-sqlite3@11.1.2)(bun-types@1.1.17)(react@18.3.1): + drizzle-orm@0.32.2(@types/pg@8.11.6)(@types/react@18.3.3)(better-sqlite3@11.1.2)(bun-types@1.1.17)(pg@8.12.0)(postgres@3.4.4)(react@18.3.1): optionalDependencies: + '@types/pg': 8.11.6 '@types/react': 18.3.3 better-sqlite3: 11.1.2 bun-types: 1.1.17 + pg: 8.12.0 + postgres: 3.4.4 react: 18.3.1 eastasianwidth@0.2.0: {} @@ -4037,6 +4141,9 @@ snapshots: object-hash@3.0.0: {} + obuf@1.1.2: + optional: true + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -4090,6 +4197,62 @@ snapshots: path-type@4.0.0: {} + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.6.4: + optional: true + + pg-int8@1.0.1: + optional: true + + pg-numeric@1.0.2: + optional: true + + pg-pool@3.6.2(pg@8.12.0): + dependencies: + pg: 8.12.0 + optional: true + + pg-protocol@1.6.1: + optional: true + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + optional: true + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + optional: true + + pg@8.12.0: + dependencies: + pg-connection-string: 2.6.4 + pg-pool: 3.6.2(pg@8.12.0) + pg-protocol: 1.6.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + optional: true + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + optional: true + picocolors@1.0.1: {} picomatch@2.3.1: {} @@ -4173,6 +4336,39 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + postgres-array@2.0.0: + optional: true + + postgres-array@3.0.2: + optional: true + + postgres-bytea@1.0.0: + optional: true + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + optional: true + + postgres-date@1.0.7: + optional: true + + postgres-date@2.1.0: + optional: true + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + optional: true + + postgres-interval@3.0.0: + optional: true + + postgres-range@1.1.4: + optional: true + + postgres@3.4.4: {} + prebuild-install@7.1.2: dependencies: detect-libc: 2.0.3 @@ -4625,6 +4821,9 @@ snapshots: wrappy@1.0.2: {} + xtend@4.0.2: + optional: true + y18n@5.0.8: {} yaml@2.5.0: {} diff --git a/server/db/index.ts b/server/db/index.ts index 7849aa5..1a8504d 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -1,10 +1,13 @@ -import { drizzle } from "drizzle-orm/bun-sqlite"; -import { Database } from "bun:sqlite"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; import * as schema from "../models"; -const DATABASE_PATH = import.meta.env.DATABASE_PATH || "./data.db"; +const DATABASE_PATH = import.meta.env.DATABASE_PATH; +if (!DATABASE_PATH) { + throw new Error("DATABASE_PATH is not set"); +} -const sqlite = new Database(DATABASE_PATH); -const db = drizzle(sqlite, { schema }); +const queryClient = postgres(DATABASE_PATH); +const db = drizzle(queryClient, { schema }); export default db; diff --git a/server/db/migrations/0000_round_hellion.sql b/server/db/migrations/0000_round_hellion.sql new file mode 100644 index 0000000..8f9403f --- /dev/null +++ b/server/db/migrations/0000_round_hellion.sql @@ -0,0 +1,59 @@ +CREATE TABLE IF NOT EXISTS "repository_languages" ( + "id" serial PRIMARY KEY NOT NULL, + "repo_id" integer NOT NULL, + "name" varchar NOT NULL, + "percentage" double precision NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "repositories" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "name" varchar NOT NULL, + "uri" varchar NOT NULL, + "language" varchar NOT NULL, + "stars" integer NOT NULL, + "forks" integer NOT NULL, + "last_update" varchar NOT NULL, + "contributors" jsonb, + "is_pending" boolean DEFAULT false NOT NULL, + "is_error" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "users" ( + "id" serial PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "name" varchar NOT NULL, + "avatar" varchar, + "location" varchar, + "followers" integer DEFAULT 0 NOT NULL, + "following" integer DEFAULT 0 NOT NULL, + "achievements" jsonb DEFAULT '[]'::jsonb, + "points" integer DEFAULT 0 NOT NULL, + "commits" integer DEFAULT 0 NOT NULL, + "line_of_codes" integer DEFAULT 0 NOT NULL, + "github_id" integer, + "access_token" varchar, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "users_username_unique" UNIQUE("username"), + CONSTRAINT "users_github_id_unique" UNIQUE("github_id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "repository_languages" ADD CONSTRAINT "repository_languages_repo_id_repositories_id_fk" FOREIGN KEY ("repo_id") REFERENCES "public"."repositories"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "repositories" ADD CONSTRAINT "repositories_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "repository_languages_name_idx" ON "repository_languages" USING btree ("name");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "repositories_name_idx" ON "repositories" USING btree ("name");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "repositories_uri_idx" ON "repositories" USING btree ("uri");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "repositories_language_idx" ON "repositories" USING btree ("language"); \ No newline at end of file diff --git a/server/db/migrations/0000_tough_jubilee.sql b/server/db/migrations/0000_tough_jubilee.sql deleted file mode 100644 index b08dfdc..0000000 --- a/server/db/migrations/0000_tough_jubilee.sql +++ /dev/null @@ -1,49 +0,0 @@ -CREATE TABLE `repository_languages` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `repo_id` integer NOT NULL, - `name` text NOT NULL, - `percentage` real NOT NULL, - FOREIGN KEY (`repo_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `repositories` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `user_id` integer NOT NULL, - `name` text NOT NULL, - `uri` text NOT NULL, - `language` text NOT NULL, - `stars` integer NOT NULL, - `forks` integer NOT NULL, - `last_update` text NOT NULL, - `contributors` text, - `is_pending` integer DEFAULT false NOT NULL, - `is_error` integer DEFAULT false NOT NULL, - `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, - `updated_at` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `users` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `username` text NOT NULL, - `name` text NOT NULL, - `avatar` text, - `location` text, - `followers` integer DEFAULT 0 NOT NULL, - `following` integer DEFAULT 0 NOT NULL, - `achievements` text DEFAULT '[]', - `points` integer DEFAULT 0 NOT NULL, - `commits` integer DEFAULT 0 NOT NULL, - `line_of_codes` integer DEFAULT 0 NOT NULL, - `github_id` integer, - `access_token` text, - `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, - `updated_at` text NOT NULL -); ---> statement-breakpoint -CREATE INDEX `repository_languages_name_idx` ON `repository_languages` (`name`);--> statement-breakpoint -CREATE INDEX `repositories_name_idx` ON `repositories` (`name`);--> statement-breakpoint -CREATE INDEX `repositories_uri_idx` ON `repositories` (`uri`);--> statement-breakpoint -CREATE INDEX `repositories_language_idx` ON `repositories` (`language`);--> statement-breakpoint -CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint -CREATE UNIQUE INDEX `users_github_id_unique` ON `users` (`github_id`); \ No newline at end of file diff --git a/server/db/migrations/meta/0000_snapshot.json b/server/db/migrations/meta/0000_snapshot.json index d8c9689..bc554a1 100644 --- a/server/db/migrations/meta/0000_snapshot.json +++ b/server/db/migrations/meta/0000_snapshot.json @@ -1,48 +1,53 @@ { - "version": "6", - "dialect": "sqlite", - "id": "a268ec23-239a-4d86-8830-2f3c1c2110df", + "id": "5131e61a-98fe-40ef-b206-62b860d39639", "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", "tables": { - "repository_languages": { + "public.repository_languages": { "name": "repository_languages", + "schema": "", "columns": { "id": { "name": "id", - "type": "integer", + "type": "serial", "primaryKey": true, - "notNull": true, - "autoincrement": true + "notNull": true }, "repo_id": { "name": "repo_id", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "name": { "name": "name", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "percentage": { "name": "percentage", - "type": "real", + "type": "double precision", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true } }, "indexes": { "repository_languages_name_idx": { "name": "repository_languages_name_idx", "columns": [ - "name" + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": { @@ -63,125 +68,137 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "repositories": { + "public.repositories": { "name": "repositories", + "schema": "", "columns": { "id": { "name": "id", - "type": "integer", + "type": "serial", "primaryKey": true, - "notNull": true, - "autoincrement": true + "notNull": true }, "user_id": { "name": "user_id", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "name": { "name": "name", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "uri": { "name": "uri", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "language": { "name": "language", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "stars": { "name": "stars", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "forks": { "name": "forks", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "last_update": { "name": "last_update", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "contributors": { "name": "contributors", - "type": "text", + "type": "jsonb", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "is_pending": { "name": "is_pending", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": false }, "is_error": { "name": "is_error", - "type": "integer", + "type": "boolean", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": false }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "CURRENT_TIMESTAMP" }, "updated_at": { "name": "updated_at", - "type": "text", + "type": "timestamp", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true } }, "indexes": { "repositories_name_idx": { "name": "repositories_name_idx", "columns": [ - "name" + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "repositories_uri_idx": { "name": "repositories_uri_idx", "columns": [ - "uri" + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "repositories_language_idx": { "name": "repositories_language_idx", "columns": [ - "language" + { + "expression": "language", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": { @@ -202,50 +219,45 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "users": { + "public.users": { "name": "users", + "schema": "", "columns": { "id": { "name": "id", - "type": "integer", + "type": "serial", "primaryKey": true, - "notNull": true, - "autoincrement": true + "notNull": true }, "username": { "name": "username", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "name": { "name": "name", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "avatar": { "name": "avatar", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "location": { "name": "location", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "followers": { "name": "followers", "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "following": { @@ -253,23 +265,20 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "achievements": { "name": "achievements", - "type": "text", + "type": "jsonb", "primaryKey": false, "notNull": false, - "autoincrement": false, - "default": "'[]'" + "default": "'[]'::jsonb" }, "points": { "name": "points", "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "commits": { @@ -277,7 +286,6 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "line_of_codes": { @@ -285,67 +293,61 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "github_id": { "name": "github_id", "type": "integer", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "access_token": { "name": "access_token", - "type": "text", + "type": "varchar", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "created_at": { "name": "created_at", - "type": "text", + "type": "timestamp", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "CURRENT_TIMESTAMP" }, "updated_at": { "name": "updated_at", - "type": "text", + "type": "timestamp", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true } }, - "indexes": { + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { "users_username_unique": { "name": "users_username_unique", + "nullsNotDistinct": false, "columns": [ "username" - ], - "isUnique": true + ] }, "users_github_id_unique": { "name": "users_github_id_unique", + "nullsNotDistinct": false, "columns": [ "github_id" - ], - "isUnique": true + ] } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} + } } }, "enums": {}, + "schemas": {}, + "sequences": {}, "_meta": { + "columns": {}, "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} + "tables": {} } } \ No newline at end of file diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json index 3cf21f1..42c3a1c 100644 --- a/server/db/migrations/meta/_journal.json +++ b/server/db/migrations/meta/_journal.json @@ -4,9 +4,9 @@ "entries": [ { "idx": 0, - "version": "6", - "when": 1723211354217, - "tag": "0000_tough_jubilee", + "version": "7", + "when": 1723216591610, + "tag": "0000_round_hellion", "breakpoints": true } ] diff --git a/server/drizzle.config.ts b/server/drizzle.config.ts index dfbfeee..5f5605e 100644 --- a/server/drizzle.config.ts +++ b/server/drizzle.config.ts @@ -1,11 +1,14 @@ import { defineConfig } from "drizzle-kit"; -const DATABASE_PATH = process.env.DATABASE_PATH || "./data.db"; +const DATABASE_PATH = process.env.DATABASE_PATH; +if (!DATABASE_PATH) { + throw new Error("DATABASE_PATH is not set"); +} export default defineConfig({ schema: "./server/models/index.ts", out: "./server/db/migrations", - dialect: "sqlite", + dialect: "postgresql", dbCredentials: { url: DATABASE_PATH }, verbose: true, }); diff --git a/server/jobs/fetch-repo-contributors.ts b/server/jobs/fetch-repo-contributors.ts index a581322..e348ba5 100644 --- a/server/jobs/fetch-repo-contributors.ts +++ b/server/jobs/fetch-repo-contributors.ts @@ -22,13 +22,15 @@ export const fetchRepoContributors = async ( throw new Error("Repository not found!"); } - if (!repo.userAccessToken) { + const accessToken = + repo.userAccessToken || import.meta.env.GITHUB_DEFAULT_TOKEN; + if (!accessToken) { throw new Error("User access token not found!"); } const contributors = await github.getRepoContributors(data.uri, { headers: { - Authorization: `Bearer ${repo.userAccessToken}`, + Authorization: `Bearer ${accessToken}`, }, }); @@ -42,7 +44,11 @@ export const fetchRepoContributors = async ( throw new Error("Cannot update repository!"); } - await queue.add("calculateUserPoints", { userId: result.userId }); + await queue.add( + "calculateUserPoints", + { userId: result.userId }, + { jobId: `calculateUserPoints:${result.userId}` } + ); }; export const onFetchRepoContribFailed = async ( diff --git a/server/jobs/fetch-repo-data.ts b/server/jobs/fetch-repo-data.ts index a5e7c4c..890cf6e 100644 --- a/server/jobs/fetch-repo-data.ts +++ b/server/jobs/fetch-repo-data.ts @@ -61,5 +61,9 @@ export const fetchRepoData = async (data: FetchRepoDataJobType) => { } }); - await queue.add("calculateUserPoints", { userId: repository.userId }); + await queue.add( + "calculateUserPoints", + { userId: repository.userId }, + { jobId: `calculateUserPoints:${repository.userId}` } + ); }; diff --git a/server/jobs/fetch-user-profile.ts b/server/jobs/fetch-user-profile.ts index c6d598a..b5efda6 100644 --- a/server/jobs/fetch-user-profile.ts +++ b/server/jobs/fetch-user-profile.ts @@ -27,5 +27,9 @@ export const fetchUserProfile = async (data: FetchUserProfileType) => { }) .where(eq(users.id, user.id)); - await queue.add("calculateUserPoints", { userId: user.id }); + await queue.add( + "calculateUserPoints", + { userId: user.id }, + { jobId: `calculateUserPoints:${user.id}` } + ); }; diff --git a/server/jobs/fetch-user-repos.ts b/server/jobs/fetch-user-repos.ts index 1bb7994..9d777f6 100644 --- a/server/jobs/fetch-user-repos.ts +++ b/server/jobs/fetch-user-repos.ts @@ -62,7 +62,7 @@ export const fetchUserRepos = async (data: FetchUserRepos) => { data, opts: { attempts: 5, - backoff: { type: "exponential", delay: 30000 }, + backoff: { type: "exponential", delay: 3000 }, jobId: `contributors:${data.uri}`, }, })) diff --git a/server/lib/github.ts b/server/lib/github.ts index 757655a..48f187d 100644 --- a/server/lib/github.ts +++ b/server/lib/github.ts @@ -32,7 +32,6 @@ const github = { const name = $(selectors.user.name).text().trim(); const avatar = $(selectors.user.avatar).attr("src"); - console.log({ avatar }); const location = $(selectors.user.location).text().trim(); const followers = intval($(selectors.user.followers).text().trim()); const following = intval($(selectors.user.following).text().trim()); diff --git a/server/models/repo-languages.ts b/server/models/repo-languages.ts index 44ac3e8..36cdd6a 100644 --- a/server/models/repo-languages.ts +++ b/server/models/repo-languages.ts @@ -1,23 +1,24 @@ import { InferInsertModel, InferSelectModel, relations } from "drizzle-orm"; import { - text, - sqliteTable, + varchar, + serial, + pgTable, integer, index, - real, -} from "drizzle-orm/sqlite-core"; + doublePrecision, +} from "drizzle-orm/pg-core"; import { repositories } from "./repositories"; -export const repoLanguages = sqliteTable( +export const repoLanguages = pgTable( "repository_languages", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: serial("id").primaryKey(), repoId: integer("repo_id") .notNull() .references(() => repositories.id), - name: text("name").notNull(), - percentage: real("percentage").notNull(), + name: varchar("name").notNull(), + percentage: doublePrecision("percentage").notNull(), }, (t) => ({ nameIdx: index("repository_languages_name_idx").on(t.name), diff --git a/server/models/repositories.ts b/server/models/repositories.ts index 0c7e20f..39248f3 100644 --- a/server/models/repositories.ts +++ b/server/models/repositories.ts @@ -5,37 +5,44 @@ import { relations, sql, } from "drizzle-orm"; -import { text, sqliteTable, integer, index } from "drizzle-orm/sqlite-core"; +import { + varchar, + pgTable, + integer, + index, + serial, + jsonb, + boolean, + timestamp, +} from "drizzle-orm/pg-core"; import { users } from "./users"; import { repoLanguages } from "./repo-languages"; -export const repositories = sqliteTable( +export const repositories = pgTable( "repositories", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: serial("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => users.id), - name: text("name").notNull(), - uri: text("uri").notNull(), - language: text("language").notNull(), + name: varchar("name").notNull(), + uri: varchar("uri").notNull(), + language: varchar("language").notNull(), stars: integer("stars").notNull(), forks: integer("forks").notNull(), - lastUpdate: text("last_update").notNull(), - contributors: text("contributors", { mode: "json" }).$type(), + lastUpdate: varchar("last_update").notNull(), + contributors: jsonb("contributors").$type(), - isPending: integer("is_pending", { mode: "boolean" }) - .notNull() - .default(false), - isError: integer("is_error", { mode: "boolean" }).notNull().default(false), + isPending: boolean("is_pending").notNull().default(false), + isError: boolean("is_error").notNull().default(false), - createdAt: text("created_at") + createdAt: timestamp("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") + updatedAt: timestamp("updated_at") .notNull() - .$onUpdate(() => sql`CURRENT_TIMESTAMP`), + .$onUpdate(() => new Date()), }, (t) => ({ nameIdx: index("repositories_name_idx").on(t.name), diff --git a/server/models/users.ts b/server/models/users.ts index 032bd14..0f14df1 100644 --- a/server/models/users.ts +++ b/server/models/users.ts @@ -1,30 +1,35 @@ import { Achievement } from "@server/lib/github"; import { InferInsertModel, InferSelectModel, sql } from "drizzle-orm"; -import { text, sqliteTable, integer } from "drizzle-orm/sqlite-core"; +import { + varchar, + pgTable, + serial, + integer, + jsonb, + timestamp, +} from "drizzle-orm/pg-core"; -export const users = sqliteTable("users", { - id: integer("id").primaryKey({ autoIncrement: true }), - username: text("username").notNull().unique(), - name: text("name").notNull(), - avatar: text("avatar"), - location: text("location"), +export const users = pgTable("users", { + id: serial("id").primaryKey(), + username: varchar("username").notNull().unique(), + name: varchar("name").notNull(), + avatar: varchar("avatar"), + location: varchar("location"), followers: integer("followers").notNull().default(0), following: integer("following").notNull().default(0), - achievements: text("achievements", { mode: "json" }) - .$type() - .default([]), + achievements: jsonb("achievements").$type().default([]), points: integer("points").notNull().default(0), commits: integer("commits").notNull().default(0), lineOfCodes: integer("line_of_codes").notNull().default(0), githubId: integer("github_id").unique(), - accessToken: text("access_token"), + accessToken: varchar("access_token"), - createdAt: text("created_at") + createdAt: timestamp("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") + updatedAt: timestamp("updated_at") .notNull() - .$onUpdate(() => sql`CURRENT_TIMESTAMP`), + .$onUpdate(() => new Date()), }); export type User = InferSelectModel; diff --git a/server/repository/leaderboard.ts b/server/repository/leaderboard.ts index 8816ec3..7729522 100644 --- a/server/repository/leaderboard.ts +++ b/server/repository/leaderboard.ts @@ -59,7 +59,8 @@ export class LeaderboardRepository { .from(repositories) .innerJoin(repoLanguages, eq(repoLanguages.repoId, repositories.id)) .groupBy(repoLanguages.name) - .orderBy(desc(sql`count(*) * avg(${repoLanguages.percentage})`)); + .orderBy(desc(sql`count(*) * avg(${repoLanguages.percentage})`)) + .limit(20); const columns = [ { @@ -116,7 +117,8 @@ export class LeaderboardRepository { .innerJoin(repoLanguages, eq(repoLanguages.repoId, repositories.id)) .where(inArray(sql`lower(${repoLanguages.name})`, languages)) .groupBy(users.id) - .orderBy(desc(count())); + .orderBy(desc(count())) + .limit(100); const columns = [ {