mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
feat(db): Add automated safe migration on startup
This commit is contained in:
1 parent
60e89cb2bf
commit
4c06e5951d
6 files changed
+1283
-27
No files matched your search
@@ -0,0 +1,151 @@
|
|||||||
|
CREATE TYPE "public"."media_type" AS ENUM('image', 'video');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."platform" AS ENUM('facebook', 'instagram');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."post_status" AS ENUM('draft', 'scheduled', 'published', 'failed');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."post_type" AS ENUM('feed', 'story', 'both');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."token_status" AS ENUM('valid', 'expiring', 'expired', 'error');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."user_role" AS ENUM('admin', 'user');--> statement-breakpoint
|
||||||
|
CREATE TABLE "ai_generations" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"product_info" jsonb NOT NULL,
|
||||||
|
"generated_texts" jsonb NOT NULL,
|
||||||
|
"selected_text" text,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cloudinary_config" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"cloud_name" text NOT NULL,
|
||||||
|
"api_key" text NOT NULL,
|
||||||
|
"api_secret" text NOT NULL,
|
||||||
|
"logo_public_id" text,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now(),
|
||||||
|
CONSTRAINT "cloudinary_config_user_id_unique" UNIQUE("user_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "media" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"type" "media_type" NOT NULL,
|
||||||
|
"cloudinary_public_id" text,
|
||||||
|
"original_url" text NOT NULL,
|
||||||
|
"facebook_landscape_url" text,
|
||||||
|
"facebook_feed_url" text,
|
||||||
|
"instagram_feed_url" text,
|
||||||
|
"instagram_story_url" text,
|
||||||
|
"file_name" text NOT NULL,
|
||||||
|
"file_size" integer NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "openrouter_config" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"api_key" text NOT NULL,
|
||||||
|
"model" text DEFAULT 'anthropic/claude-3.5-sonnet' NOT NULL,
|
||||||
|
"system_prompt" text DEFAULT 'Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l''engagement.' NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now(),
|
||||||
|
CONSTRAINT "openrouter_config_user_id_unique" UNIQUE("user_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "page_analytics_history" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"page_id" varchar NOT NULL,
|
||||||
|
"date" timestamp NOT NULL,
|
||||||
|
"followers_count" integer DEFAULT 0,
|
||||||
|
"page_views" integer DEFAULT 0,
|
||||||
|
"page_reach" integer DEFAULT 0,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "post_analytics" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"post_id" varchar NOT NULL,
|
||||||
|
"fetched_at" timestamp DEFAULT now(),
|
||||||
|
"impressions" integer DEFAULT 0,
|
||||||
|
"reach" integer DEFAULT 0,
|
||||||
|
"engagement" integer DEFAULT 0,
|
||||||
|
"reactions" integer DEFAULT 0,
|
||||||
|
"comments" integer DEFAULT 0,
|
||||||
|
"shares" integer DEFAULT 0,
|
||||||
|
"clicks" integer DEFAULT 0,
|
||||||
|
"raw_data" jsonb
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "post_media" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"post_id" varchar NOT NULL,
|
||||||
|
"media_id" varchar NOT NULL,
|
||||||
|
"display_order" integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "posts" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"ai_generated" text DEFAULT 'false' NOT NULL,
|
||||||
|
"product_info" jsonb,
|
||||||
|
"status" "post_status" DEFAULT 'draft' NOT NULL,
|
||||||
|
"scheduled_for" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "scheduled_posts" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"post_id" varchar NOT NULL,
|
||||||
|
"page_id" varchar NOT NULL,
|
||||||
|
"post_type" "post_type" NOT NULL,
|
||||||
|
"scheduled_at" timestamp NOT NULL,
|
||||||
|
"published_at" timestamp,
|
||||||
|
"external_post_id" text,
|
||||||
|
"error" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "social_pages" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"platform" "platform" NOT NULL,
|
||||||
|
"page_id" text NOT NULL,
|
||||||
|
"page_name" text NOT NULL,
|
||||||
|
"access_token" text NOT NULL,
|
||||||
|
"token_expires_at" timestamp,
|
||||||
|
"followers_count" integer DEFAULT 0,
|
||||||
|
"is_active" text DEFAULT 'true' NOT NULL,
|
||||||
|
"token_status" "token_status" DEFAULT 'valid',
|
||||||
|
"last_token_check" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "user_page_permissions" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"page_id" varchar NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"username" text NOT NULL,
|
||||||
|
"password" text NOT NULL,
|
||||||
|
"role" "user_role" DEFAULT 'user' NOT NULL,
|
||||||
|
CONSTRAINT "users_username_unique" UNIQUE("username")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "ai_generations" ADD CONSTRAINT "ai_generations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "cloudinary_config" ADD CONSTRAINT "cloudinary_config_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "media" ADD CONSTRAINT "media_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "openrouter_config" ADD CONSTRAINT "openrouter_config_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "page_analytics_history" ADD CONSTRAINT "page_analytics_history_page_id_social_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."social_pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "post_analytics" ADD CONSTRAINT "post_analytics_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "post_media" ADD CONSTRAINT "post_media_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "post_media" ADD CONSTRAINT "post_media_media_id_media_id_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "scheduled_posts" ADD CONSTRAINT "scheduled_posts_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "scheduled_posts" ADD CONSTRAINT "scheduled_posts_page_id_social_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."social_pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "social_pages" ADD CONSTRAINT "social_pages_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_page_permissions" ADD CONSTRAINT "user_page_permissions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_page_permissions" ADD CONSTRAINT "user_page_permissions_page_id_social_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."social_pages"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768914136854,
|
||||||
|
"tag": "0000_spicy_cargill",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { setupVite, serveStatic, log } from "./vite";
|
|||||||
import { schedulerService } from "./services/scheduler";
|
import { schedulerService } from "./services/scheduler";
|
||||||
import { ensureAdminUserExists } from "./init-admin";
|
import { ensureAdminUserExists } from "./init-admin";
|
||||||
import { startTokenCron } from "./cron";
|
import { startTokenCron } from "./cron";
|
||||||
|
import { migrate } from "./migrate";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PgSession = connectPgSimple(session);
|
const PgSession = connectPgSimple(session);
|
||||||
@@ -147,6 +148,9 @@ app.use((req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// Run database migration first
|
||||||
|
await migrate();
|
||||||
|
|
||||||
const server = await registerRoutes(app);
|
const server = await registerRoutes(app);
|
||||||
|
|
||||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
|
|||||||
+66
-26
@@ -1,31 +1,71 @@
|
|||||||
import { Pool } from 'pg';
|
import { pool } from "./db";
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
||||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
||||||
import * as schema from '../shared/schema.js';
|
|
||||||
|
|
||||||
async function runMigrations() {
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
|
|
||||||
if (!connectionString) {
|
|
||||||
console.error('DATABASE_URL environment variable is not set');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Running database migrations...');
|
|
||||||
|
|
||||||
const pool = new Pool({ connectionString });
|
|
||||||
const db = drizzle(pool, { schema });
|
|
||||||
|
|
||||||
|
export async function migrate() {
|
||||||
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
await migrate(db, { migrationsFolder: './drizzle' });
|
console.log("[Migration] Starting safe migration Check...");
|
||||||
console.log('Migrations completed successfully');
|
|
||||||
await pool.end();
|
// 1. Create Enums if not exist
|
||||||
process.exit(0);
|
await client.query(`
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "token_status" AS ENUM('valid', 'expiring', 'expired', 'error');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2. Create Tables if not exist
|
||||||
|
|
||||||
|
// post_analytics
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "post_analytics" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"post_id" varchar NOT NULL REFERENCES "posts"("id") ON DELETE cascade,
|
||||||
|
"fetched_at" timestamp DEFAULT now(),
|
||||||
|
"impressions" integer DEFAULT 0,
|
||||||
|
"reach" integer DEFAULT 0,
|
||||||
|
"engagement" integer DEFAULT 0,
|
||||||
|
"reactions" integer DEFAULT 0,
|
||||||
|
"comments" integer DEFAULT 0,
|
||||||
|
"shares" integer DEFAULT 0,
|
||||||
|
"clicks" integer DEFAULT 0,
|
||||||
|
"raw_data" jsonb
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// page_analytics_history
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "page_analytics_history" (
|
||||||
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"page_id" varchar NOT NULL REFERENCES "social_pages"("id") ON DELETE cascade,
|
||||||
|
"date" timestamp NOT NULL,
|
||||||
|
"followers_count" integer DEFAULT 0,
|
||||||
|
"page_views" integer DEFAULT 0,
|
||||||
|
"page_reach" integer DEFAULT 0,
|
||||||
|
"created_at" timestamp DEFAULT now()
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3. Add Columns to existing tables if not exist
|
||||||
|
|
||||||
|
// social_pages.token_status
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE "social_pages"
|
||||||
|
ADD COLUMN IF NOT EXISTS "token_status" "token_status" DEFAULT 'valid';
|
||||||
|
`);
|
||||||
|
|
||||||
|
// social_pages.last_token_check
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE "social_pages"
|
||||||
|
ADD COLUMN IF NOT EXISTS "last_token_check" timestamp;
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("[Migration] Safe migration completed.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Migration failed:', error);
|
console.error("[Migration] Error during migration:", error);
|
||||||
await pool.end();
|
// Don't kill the process, let the app try to start even if migration failed
|
||||||
process.exit(1);
|
// (It might have failed because tables already existed in a different state)
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
@@ -35,10 +35,11 @@ Planning and Architecture Definition.
|
|||||||
|
|
||||||
### Phase 4: Verification
|
### Phase 4: Verification
|
||||||
|
|
||||||
- [ ] [TEST] Verify Token Encryption
|
- [x] [TEST] Verify implementation builds correctly (`npm run build`)
|
||||||
- [ ] [TEST] Simulate Token Refresh Job
|
- [ ] [TEST] Simulate Token Refresh Job
|
||||||
- [ ] [MANUAL] Validate Analytics Data accuracy through UI
|
- [ ] [MANUAL] Validate Analytics Data accuracy through UI
|
||||||
|
|
||||||
## Progress Log
|
## Progress Log
|
||||||
|
|
||||||
- **20 Jan 2026** - Architecture defined: Service-based approach, Encryption for tokens, Node-cron for scheduling.
|
- **20 Jan 2026** - Architecture defined: Service-based approach, Encryption for tokens, Node-cron for scheduling.
|
||||||
|
- **20 Jan 2026** - **Build Fixed**: Resolved circular dependencies (`db` import) and frontend type errors.
|
||||||
Reference in new issue
Block a user