diff --git a/Dockerfile b/Dockerfile index ca0d878..275fbde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,9 @@ RUN rm -rf /app/data # Seed veritabanları: entrypoint açılışta bunları /app/data volume'üne kopyalar COPY data/ ./seed/ +# Migration'lar: entrypoint açılışta db-goc.mjs ile volume'deki app.db'ye uygular +COPY drizzle ./drizzle +COPY scripts/db-goc.mjs ./scripts/db-goc.mjs COPY scripts/docker-entrypoint.sh ./docker-entrypoint.sh # SQLite veri dizini diff --git a/drizzle/0000_baslangic.sql b/drizzle/0000_baslangic.sql new file mode 100644 index 0000000..6df59e5 --- /dev/null +++ b/drizzle/0000_baslangic.sql @@ -0,0 +1,131 @@ +CREATE TABLE `account` ( + `id` text PRIMARY KEY NOT NULL, + `account_id` text NOT NULL, + `provider_id` text NOT NULL, + `user_id` text NOT NULL, + `access_token` text, + `refresh_token` text, + `id_token` text, + `access_token_expires_at` integer, + `refresh_token_expires_at` integer, + `scope` text, + `password` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `chat_messages` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL, + `client_message_id` text, + `created_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `chat_messages_client_message_id_unique` ON `chat_messages` (`client_message_id`);--> statement-breakpoint +CREATE INDEX `chat_user_created` ON `chat_messages` (`user_id`,`created_at`);--> statement-breakpoint +CREATE TABLE `credit_ledger` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `delta` integer NOT NULL, + `reason` text NOT NULL, + `ref_id` text, + `created_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `ledger_reason_ref` ON `credit_ledger` (`reason`,`ref_id`);--> statement-breakpoint +CREATE INDEX `ledger_user` ON `credit_ledger` (`user_id`);--> statement-breakpoint +CREATE TABLE `orders` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `product` text NOT NULL, + `amount_kurus` integer NOT NULL, + `credits` integer NOT NULL, + `status` text DEFAULT 'pending' NOT NULL, + `iyzico_token` text, + `iyzico_payment_id` text, + `created_at` integer NOT NULL, + `paid_at` integer, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `orders_user` ON `orders` (`user_id`);--> statement-breakpoint +CREATE TABLE `reports` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `params` text NOT NULL, + `result` text, + `revision_count` integer DEFAULT 0 NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `reports_user_id_unique` ON `reports` (`user_id`);--> statement-breakpoint +CREATE TABLE `saved_lists` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `items` text NOT NULL, + `profil` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `saved_lists_user_id_unique` ON `saved_lists` (`user_id`);--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `expires_at` integer NOT NULL, + `token` text NOT NULL, + `ip_address` text, + `user_agent` text, + `user_id` text NOT NULL, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint +CREATE TABLE `tadimlik_havuzu` ( + `id` text PRIMARY KEY NOT NULL, + `kova_slug` text NOT NULL, + `tur` text NOT NULL, + `kategori_slug` text NOT NULL, + `program_id` text NOT NULL, + `dilim` text NOT NULL, + `gerekce` text NOT NULL, + `risk_notu` text NOT NULL, + `trend_ozeti` text NOT NULL, + `program` text NOT NULL, + `ornek_sira` integer NOT NULL, + `uretim_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `tadimlik_anahtar` ON `tadimlik_havuzu` (`kova_slug`,`tur`,`kategori_slug`);--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `email_verified` integer DEFAULT false NOT NULL, + `image` text, + `credit_balance` integer DEFAULT 0 NOT NULL, + `has_paket` integer DEFAULT false NOT NULL, + `kredi_bitti_at` integer, + `kredi_hatirlatma_gonderildi_at` integer, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint +CREATE TABLE `verification` ( + `id` text PRIMARY KEY NOT NULL, + `identifier` text NOT NULL, + `value` text NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer, + `updated_at` integer +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..231e297 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,898 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a802fded-01d5-4d17-8b6f-fca5dfc2920d", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_message_id": { + "name": "client_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_client_message_id_unique": { + "name": "chat_messages_client_message_id_unique", + "columns": [ + "client_message_id" + ], + "isUnique": true + }, + "chat_user_created": { + "name": "chat_user_created", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_user_id_user_id_fk": { + "name": "chat_messages_user_id_user_id_fk", + "tableFrom": "chat_messages", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credit_ledger": { + "name": "credit_ledger", + "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 + }, + "delta": { + "name": "delta", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "ledger_reason_ref": { + "name": "ledger_reason_ref", + "columns": [ + "reason", + "ref_id" + ], + "isUnique": true + }, + "ledger_user": { + "name": "ledger_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "orders": { + "name": "orders", + "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 + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_kurus": { + "name": "amount_kurus", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "iyzico_token": { + "name": "iyzico_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "iyzico_payment_id": { + "name": "iyzico_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "orders_user": { + "name": "orders_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "orders_user_id_user_id_fk": { + "name": "orders_user_id_user_id_fk", + "tableFrom": "orders", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "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 + }, + "params": { + "name": "params", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revision_count": { + "name": "revision_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reports_user_id_unique": { + "name": "reports_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "reports_user_id_user_id_fk": { + "name": "reports_user_id_user_id_fk", + "tableFrom": "reports", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_lists": { + "name": "saved_lists", + "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 + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profil": { + "name": "profil", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saved_lists_user_id_unique": { + "name": "saved_lists_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "saved_lists_user_id_user_id_fk": { + "name": "saved_lists_user_id_user_id_fk", + "tableFrom": "saved_lists", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tadimlik_havuzu": { + "name": "tadimlik_havuzu", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kova_slug": { + "name": "kova_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tur": { + "name": "tur", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kategori_slug": { + "name": "kategori_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dilim": { + "name": "dilim", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gerekce": { + "name": "gerekce", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "risk_notu": { + "name": "risk_notu", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trend_ozeti": { + "name": "trend_ozeti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "program": { + "name": "program", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ornek_sira": { + "name": "ornek_sira", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uretim_at": { + "name": "uretim_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tadimlik_anahtar": { + "name": "tadimlik_anahtar", + "columns": [ + "kova_slug", + "tur", + "kategori_slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_paket": { + "name": "has_paket", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "kredi_bitti_at": { + "name": "kredi_bitti_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kredi_hatirlatma_gonderildi_at": { + "name": "kredi_hatirlatma_gonderildi_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "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..465324e --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1786053613118, + "tag": "0000_baslangic", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 4196465..3508dfd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "detay": "tsx scripts/detay.ts", "logolar": "tsx scripts/logo-indir.ts", "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "node scripts/db-goc.mjs", "tadimlik": "tsx scripts/tadimlik-uret.ts" }, "dependencies": { diff --git a/scripts/db-goc.mjs b/scripts/db-goc.mjs new file mode 100644 index 0000000..d48c867 --- /dev/null +++ b/scripts/db-goc.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +/** + * app.db şemasını drizzle/ altındaki migration'larla eşitler — konteyner + * açılışında (docker-entrypoint.sh) node server.js'den ÖNCE koşar, lokalde + * `pnpm db:migrate` ile çalıştırılır. Idempotenttir. + * + * Neden drizzle-orm'un kendi migrate()'i değil: standalone çıktıda drizzle-orm + * Turbopack'çe bundle'landığı için /app/node_modules'ta paket olarak yok; + * @libsql/client ise serverExternalPackages'ta olduğundan her zaman çözülür. + * Bu script drizzle migrator protokolünü birebir taklit eder (__drizzle_migrations + * tablosu, sha256 hash, created_at = journal.when) — ileride resmi migrate()'e + * geçiş kayıtları bozmaz. + * + * Benimseme (adoption): migration akışından önce doldurulmuş prod volume'lerinde + * __drizzle_migrations tablosu yoktur ama şema (kısmen) vardır. Bu durumda + * baseline (0000) bir kez additive uygulanır — CREATE'ler IF NOT EXISTS'e + * çevrilir, eksik kolonlar snapshot'tan ALTER TABLE ile eklenir — ve baseline + * uygulanmış olarak damgalanır. Sonraki migration'lar (0001+) normal akışla gelir. + */ +import { createClient } from "@libsql/client"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import process from "node:process"; + +const GOC_KLASORU = process.env.DRIZZLE_DIR ?? join(process.cwd(), "drizzle"); +const DB_URL = process.env.APP_DB_URL ?? "file:./data/app.db"; + +const GOC_TABLOSU_DDL = `CREATE TABLE IF NOT EXISTS "__drizzle_migrations" ( + id SERIAL PRIMARY KEY, + hash text NOT NULL, + created_at numeric +)`; + +/** Journal'daki her girdiyi { when, tag, hash, ifadeler[] } olarak yükler. */ +function gocleriOku() { + const journal = JSON.parse( + readFileSync(join(GOC_KLASORU, "meta", "_journal.json"), "utf8"), + ); + return journal.entries.map((girdi) => { + const sql = readFileSync(join(GOC_KLASORU, `${girdi.tag}.sql`), "utf8"); + return { + when: girdi.when, + tag: girdi.tag, + hash: createHash("sha256").update(sql).digest("hex"), + ifadeler: sql + .split("--> statement-breakpoint") + .map((s) => s.trim()) + .filter(Boolean), + }; + }); +} + +async function tabloVar(db, ad) { + const r = await db.execute({ + sql: "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", + args: [ad], + }); + return r.rows.length > 0; +} + +/** Snapshot'taki default değerini SQL literaline çevirir (string zaten tırnaklı gelir). */ +function sqlVarsayilan(deger) { + return typeof deger === "string" ? deger : String(deger); +} + +/** + * Benimseme adımı: baseline'ı additive uygular. + * 1) CREATE TABLE/INDEX ifadeleri IF NOT EXISTS ile koşulur (eksik tablolar gelir). + * 2) Mevcut tablolarda eksik kolonlar baseline snapshot'ından ALTER TABLE ile eklenir. + */ +async function baselineIleUzlastir(db, baseline) { + for (const ifade of baseline.ifadeler) { + const korunmus = ifade + .replace(/^CREATE TABLE `/, "CREATE TABLE IF NOT EXISTS `") + .replace(/^CREATE INDEX `/, "CREATE INDEX IF NOT EXISTS `") + .replace(/^CREATE UNIQUE INDEX `/, "CREATE UNIQUE INDEX IF NOT EXISTS `"); + await db.execute(korunmus); + } + + const snapshot = JSON.parse( + readFileSync(join(GOC_KLASORU, "meta", "0000_snapshot.json"), "utf8"), + ); + for (const tablo of Object.values(snapshot.tables)) { + const mevcut = await db.execute(`PRAGMA table_info(\`${tablo.name}\`)`); + const mevcutKolonlar = new Set(mevcut.rows.map((r) => r.name)); + for (const kolon of Object.values(tablo.columns)) { + if (mevcutKolonlar.has(kolon.name)) continue; + let ddl = `ALTER TABLE \`${tablo.name}\` ADD COLUMN \`${kolon.name}\` ${kolon.type}`; + if (kolon.default !== undefined) { + ddl += ` DEFAULT ${sqlVarsayilan(kolon.default)}`; + if (kolon.notNull) ddl += " NOT NULL"; + } else if (kolon.notNull) { + // Dolu tabloya default'suz NOT NULL kolon eklenemez (SQLite kısıtı); + // nullable ekleyip uyarıyoruz — uygulama zaten değeri hep yazıyor. + console.warn( + `[db-goc] uyarı: ${tablo.name}.${kolon.name} NOT NULL ama default'suz — nullable eklendi`, + ); + } + await db.execute(ddl); + console.log(`[db-goc] kolon eklendi: ${tablo.name}.${kolon.name}`); + } + } +} + +async function main() { + const gocler = gocleriOku(); + const db = createClient({ + url: DB_URL, + authToken: process.env.APP_DB_AUTH_TOKEN, + }); + + try { + // Benimseme: şema var ama migration kaydı yok → baseline'ı uzlaştır + damgala. + const kayitVar = await tabloVar(db, "__drizzle_migrations"); + if (!kayitVar && (await tabloVar(db, "user"))) { + console.log("[db-goc] migration kaydı yok, mevcut şema benimseniyor..."); + await baselineIleUzlastir(db, gocler[0]); + await db.execute(GOC_TABLOSU_DDL); + await db.execute({ + sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)', + args: [gocler[0].hash, gocler[0].when], + }); + console.log(`[db-goc] baseline damgalandı: ${gocler[0].tag}`); + } + + // Normal akış: son uygulanan migration'dan sonrakileri sırayla uygula. + await db.execute(GOC_TABLOSU_DDL); + const son = await db.execute( + 'SELECT created_at FROM "__drizzle_migrations" ORDER BY created_at DESC LIMIT 1', + ); + const sonZaman = son.rows.length ? Number(son.rows[0].created_at) : 0; + + for (const goc of gocler) { + if (goc.when <= sonZaman) continue; + await db.batch( + [ + ...goc.ifadeler, + { + sql: 'INSERT INTO "__drizzle_migrations" (hash, created_at) VALUES (?, ?)', + args: [goc.hash, goc.when], + }, + ], + "write", + ); + console.log(`[db-goc] uygulandı: ${goc.tag}`); + } + console.log("[db-goc] şema güncel"); + } finally { + db.close(); + } +} + +main().catch((hata) => { + console.error("[db-goc] migration başarısız:", hata); + process.exit(1); +}); diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index c5ac291..6aa5f2f 100644 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -9,9 +9,14 @@ for ek in wal shm; do fi done -# app.db kullanıcı verisi: sadece volume'de yoksa (ilk kurulum) şema tohumu kopyalanır +# app.db kullanıcı verisi: sadece volume'de yoksa (ilk kurulum) tohum kopyalanır +# (tohum tadimlik_havuzu içeriği de taşır, salt şema değil) if [ ! -s /app/data/app.db ] && [ -f /app/seed/app.db ]; then cp /app/seed/app.db /app/data/ fi +# Volume'deki app.db şeması güncel şemayla eşitlenir: drizzle/ altındaki +# migration'lar uygulanır (idempotent; ilk koşuda mevcut şema benimsenir). +node /app/scripts/db-goc.mjs + exec node server.js diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 03e0d91..4771613 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -6,10 +6,8 @@ export async function register() { if (process.env.NEXT_RUNTIME !== "nodejs") return; if (process.env.NEXT_PHASE === "phase-production-build") return; - // Migration akışı olmadığından sonradan eklenen tablolar açılışta güvenceye - // alınır (additive DDL, idempotent) — mevcut prod volume'ü seed'den kopyalanmaz. - const { semayiGuvenceyeAl } = await import("./lib/appdb/sema-guvence"); - await semayiGuvenceyeAl(); + // Şema, konteyner açılışında scripts/db-goc.mjs'in uyguladığı drizzle + // migration'larıyla güncellenir (bkz. docker-entrypoint.sh); burada DDL koşmaz. const { krediHatirlatmaBaslat } = await import("./lib/kredi-hatirlatma"); krediHatirlatmaBaslat(); } diff --git a/src/lib/appdb/schema.ts b/src/lib/appdb/schema.ts index 4021086..6ca8a7f 100644 --- a/src/lib/appdb/schema.ts +++ b/src/lib/appdb/schema.ts @@ -156,7 +156,8 @@ export const reports = sqliteTable("reports", { // Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları. // Anahtar: sıra kovası × puan türü × kategori ("genel" = kategorisiz fallback). // Anonim istek LLM'e asla gitmez; bu tablo salt-okunur servis edilir. -// NOT: Yeni tablo/kolon eklerken sema-guvence.ts'teki DDL de güncellenmeli. +// NOT: Yeni tablo/kolon eklerken `pnpm db:generate` ile migration üretilmeli; +// prod şeması açılışta yalnızca drizzle/ altındaki migration'larla güncellenir. export const tadimlikHavuzu = sqliteTable( "tadimlik_havuzu", { diff --git a/src/lib/appdb/sema-guvence.ts b/src/lib/appdb/sema-guvence.ts deleted file mode 100644 index 7a9f43f..0000000 --- a/src/lib/appdb/sema-guvence.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { sql } from "drizzle-orm"; -import { appDb } from "./index"; - -/** - * Prod volume'ündeki app.db'ye sonradan eklenen tabloları güvenceye alır. - * - * Projede drizzle migration akışı yok: şema lokalde `pnpm db:push` ile - * uygulanıp data/app.db seed olarak taşınıyor; ama docker-entrypoint.sh - * app.db'yi yalnızca volume BOŞKEN kopyalar — mevcut prod volume'üne yeni - * tablolar başka türlü gelmez. Burası sunucu açılışında (instrumentation) - * bir kez koşar; yalnızca additive DDL içerir, idempotenttir. - * - * NOT: schema.ts'e tablo/kolon eklendiğinde buradaki DDL de güncellenmeli. - */ -export async function semayiGuvenceyeAl(): Promise { - await appDb.run(sql` - CREATE TABLE IF NOT EXISTS tadimlik_havuzu ( - id TEXT PRIMARY KEY, - kova_slug TEXT NOT NULL, - tur TEXT NOT NULL, - kategori_slug TEXT NOT NULL, - program_id TEXT NOT NULL, - dilim TEXT NOT NULL, - gerekce TEXT NOT NULL, - risk_notu TEXT NOT NULL, - trend_ozeti TEXT NOT NULL, - program TEXT NOT NULL, - ornek_sira INTEGER NOT NULL, - uretim_at INTEGER NOT NULL - ) - `); - await appDb.run( - sql`CREATE UNIQUE INDEX IF NOT EXISTS tadimlik_anahtar ON tadimlik_havuzu (kova_slug, tur, kategori_slug)`, - ); - await appDb.run(sql` - CREATE TABLE IF NOT EXISTS saved_lists ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL UNIQUE REFERENCES user(id), - items TEXT NOT NULL, - profil TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); -}