Seth commited on
Commit
4b3843b
·
1 Parent(s): 3131fa3
.DS_Store ADDED
Binary file (10.2 kB). View file
 
backend/.DS_Store ADDED
Binary file (8.2 kB). View file
 
backend/app/.DS_Store ADDED
Binary file (6.15 kB). View file
 
backend/app/database.py CHANGED
@@ -1,354 +1,360 @@
1
- import os
2
- from sqlalchemy import create_engine, text
3
- from sqlalchemy.orm import sessionmaker, declarative_base
4
- from sqlalchemy.pool import NullPool
5
-
6
- # Try to use CockroachDB dialect if available
7
- try:
8
- import cockroachdb.sqlalchemy.dialect
9
- COCKROACHDB_AVAILABLE = True
10
- except ImportError:
11
- COCKROACHDB_AVAILABLE = False
12
-
13
- # Get database URL from environment variable
14
- # Default to SQLite for local development if not set
15
- ORIGINAL_DATABASE_URL = os.getenv(
16
- "DATABASE_URL",
17
- "sqlite:///./postgen.db"
18
- )
19
- DATABASE_URL = ORIGINAL_DATABASE_URL
20
-
21
- # For CockroachDB, we need to handle SSL and connection pooling
22
- if ORIGINAL_DATABASE_URL.startswith("postgresql://") or ORIGINAL_DATABASE_URL.startswith("postgres://") or ORIGINAL_DATABASE_URL.startswith("cockroachdb://"):
23
- # Check if this is a CockroachDB connection (use original URL before modifications)
24
- is_cockroach = "cockroachlabs" in ORIGINAL_DATABASE_URL.lower()
25
-
26
- # CockroachDB connection - use NullPool to avoid connection issues
27
- # CockroachDB requires SSL, so we ensure sslmode is set
28
- # Use 'require' mode which uses SSL but doesn't require certificate file
29
- # For production with certificate, use 'verify-full' and provide sslrootcert
30
- cert_path = os.path.expanduser("~/.postgresql/root.crt")
31
-
32
- if "sslmode" not in DATABASE_URL:
33
- separator = "&" if "?" in DATABASE_URL else "?"
34
- # Use 'require' instead of 'verify-full' to work without certificate file
35
- # Still secure (uses SSL) but doesn't verify the certificate
36
- DATABASE_URL = f"{DATABASE_URL}{separator}sslmode=require"
37
- elif "sslmode=verify-full" in DATABASE_URL and not os.path.exists(cert_path):
38
- # If verify-full is set but cert file doesn't exist, change to require
39
- DATABASE_URL = DATABASE_URL.replace("sslmode=verify-full", "sslmode=require")
40
- print("⚠ Certificate file not found, using sslmode=require instead of verify-full")
41
-
42
- # Use CockroachDB dialect if available and this is a CockroachDB connection
43
- if is_cockroach and COCKROACHDB_AVAILABLE:
44
- # Replace postgresql:// with cockroachdb:// to use CockroachDB dialect
45
- DATABASE_URL = DATABASE_URL.replace("postgresql://", "cockroachdb://", 1)
46
- DATABASE_URL = DATABASE_URL.replace("postgres://", "cockroachdb://", 1)
47
-
48
- # Configure engine
49
- engine = create_engine(
50
- DATABASE_URL,
51
- poolclass=NullPool, # CockroachDB works better with NullPool
52
- echo=False, # Set to True for SQL query debugging
53
- connect_args={} # No special connect args needed
54
- )
55
- else:
56
- # SQLite for local development
57
- engine = create_engine(
58
- DATABASE_URL,
59
- connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
60
- )
61
-
62
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
63
-
64
- Base = declarative_base()
65
-
66
- def get_db():
67
- """Dependency to get database session"""
68
- db = SessionLocal()
69
- try:
70
- yield db
71
- finally:
72
- db.close()
73
-
74
- def get_direct_psycopg2_connection():
75
- """Get a direct psycopg2 connection bypassing SQLAlchemy version parsing"""
76
- try:
77
- import psycopg2
78
- from urllib.parse import urlparse, parse_qs
79
-
80
- # Only for PostgreSQL/CockroachDB connections
81
- if not (ORIGINAL_DATABASE_URL.startswith("postgresql://") or
82
- ORIGINAL_DATABASE_URL.startswith("postgres://") or
83
- ORIGINAL_DATABASE_URL.startswith("cockroachdb://")):
84
- return None
85
-
86
- # Parse connection string
87
- url_for_parsing = ORIGINAL_DATABASE_URL.replace("cockroachdb://", "postgresql://")
88
- parsed = urlparse(url_for_parsing)
89
- dbname = parsed.path[1:] if parsed.path else "defaultdb"
90
- user = parsed.username
91
- password = parsed.password
92
- host = parsed.hostname
93
- port = parsed.port or 26257
94
-
95
- # Get sslmode from query params
96
- params = parse_qs(parsed.query)
97
- sslmode_list = params.get('sslmode', ['require'])
98
- sslmode = sslmode_list[0] if sslmode_list else 'require'
99
-
100
- # Create connection
101
- conn = psycopg2.connect(
102
- dbname=dbname,
103
- user=user,
104
- password=password,
105
- host=host,
106
- port=port,
107
- sslmode=sslmode
108
- )
109
- return conn
110
- except Exception as e:
111
- print(f"Failed to create direct psycopg2 connection: {e}")
112
- return None
113
-
114
- def ensure_default_user():
115
- """Ensure a default user (id=1) exists in the database"""
116
- try:
117
- conn = get_direct_psycopg2_connection()
118
- if not conn:
119
- return 1 # Return default ID if connection fails
120
-
121
- try:
122
- cursor = conn.cursor()
123
- # Check if user with id=1 exists
124
- cursor.execute("SELECT id FROM users WHERE id = 1")
125
- if cursor.fetchone():
126
- cursor.close()
127
- conn.close()
128
- return 1
129
-
130
- # Create default user if it doesn't exist
131
- # Try with ON CONFLICT first (PostgreSQL/CockroachDB)
132
- try:
133
- cursor.execute("""
134
- INSERT INTO users (id, email, name, created_at)
135
- VALUES (1, 'default@postgen.app', 'Default User', NOW())
136
- ON CONFLICT (id) DO NOTHING
137
- """)
138
- except Exception:
139
- # If ON CONFLICT fails, try without it (might be unique constraint on email)
140
- try:
141
- cursor.execute("""
142
- INSERT INTO users (id, email, name, created_at)
143
- VALUES (1, 'default@postgen.app', 'Default User', NOW())
144
- """)
145
- except Exception as insert_error:
146
- # User might already exist (race condition), check again
147
- cursor.execute("SELECT id FROM users WHERE id = 1 OR email = 'default@postgen.app' LIMIT 1")
148
- row = cursor.fetchone()
149
- if row:
150
- cursor.close()
151
- conn.close()
152
- return row[0]
153
- # If still fails, re-raise
154
- raise insert_error
155
-
156
- conn.commit()
157
- cursor.close()
158
- conn.close()
159
- return 1
160
- except Exception as e:
161
- # If everything fails, try to get any existing user or return default
162
- try:
163
- cursor.execute("SELECT id FROM users LIMIT 1")
164
- row = cursor.fetchone()
165
- cursor.close()
166
- conn.close()
167
- if row:
168
- return row[0]
169
- except:
170
- if conn:
171
- conn.close()
172
- print(f"Warning: Could not ensure default user: {e}")
173
- return 1 # Return default ID as fallback
174
- except Exception as e:
175
- print(f"Error ensuring default user: {e}")
176
- return 1 # Return default ID as fallback
177
-
178
- def init_db():
179
- """Initialize database tables"""
180
- try:
181
- from app.models import User, Integration, Asset, Post, Campaign
182
-
183
- # Try to create tables
184
- # For CockroachDB, version parsing may fail but connection still works
185
- try:
186
- Base.metadata.create_all(bind=engine)
187
- print("✓ Database tables created successfully")
188
- return True
189
- except Exception as create_error:
190
- error_str = str(create_error)
191
- # Check if it's a version parsing error (non-fatal for CockroachDB)
192
- if "Could not determine version" in error_str:
193
- # Version parsing failed, but CockroachDB connection works
194
- # Use psycopg2 directly to bypass SQLAlchemy's version parsing
195
- try:
196
- import psycopg2
197
- from urllib.parse import urlparse, parse_qs
198
-
199
- # Parse connection string to get connection parameters
200
- # Handle both cockroachdb:// and postgresql:// schemes
201
- # Use original URL before any modifications
202
- url_for_parsing = ORIGINAL_DATABASE_URL.replace("cockroachdb://", "postgresql://")
203
- parsed = urlparse(url_for_parsing)
204
- dbname = parsed.path[1:] if parsed.path else "defaultdb"
205
- user = parsed.username
206
- password = parsed.password
207
- host = parsed.hostname
208
- port = parsed.port or 26257
209
-
210
- # Get sslmode from query params (use require as default for CockroachDB)
211
- params = parse_qs(parsed.query)
212
- sslmode_list = params.get('sslmode', ['require'])
213
- sslmode = sslmode_list[0] if sslmode_list else 'require'
214
-
215
- # Connect directly with psycopg2 (bypasses SQLAlchemy version parsing)
216
- conn = psycopg2.connect(
217
- dbname=dbname,
218
- user=user,
219
- password=password,
220
- host=host,
221
- port=port,
222
- sslmode=sslmode
223
- )
224
-
225
- cursor = conn.cursor()
226
- # Create tables using IF NOT EXISTS
227
- tables_sql = [
228
- """CREATE TABLE IF NOT EXISTS users (
229
- id SERIAL PRIMARY KEY,
230
- email VARCHAR UNIQUE,
231
- name VARCHAR,
232
- created_at TIMESTAMP DEFAULT NOW()
233
- )""",
234
- """CREATE TABLE IF NOT EXISTS integrations (
235
- id SERIAL PRIMARY KEY,
236
- user_id INTEGER REFERENCES users(id),
237
- provider VARCHAR,
238
- access_token TEXT,
239
- refresh_token TEXT,
240
- expires_at TIMESTAMP,
241
- account_info JSONB,
242
- connected BOOLEAN DEFAULT FALSE,
243
- created_at TIMESTAMP DEFAULT NOW(),
244
- updated_at TIMESTAMP DEFAULT NOW()
245
- )""",
246
- """CREATE TABLE IF NOT EXISTS assets (
247
- id SERIAL PRIMARY KEY,
248
- user_id INTEGER REFERENCES users(id),
249
- name VARCHAR,
250
- file_path VARCHAR,
251
- file_type VARCHAR,
252
- product_category VARCHAR,
253
- sub_category VARCHAR,
254
- size INTEGER,
255
- extra_metadata JSONB,
256
- extracted_content JSONB,
257
- analysis_status VARCHAR DEFAULT 'pending',
258
- analyzed_at TIMESTAMP,
259
- created_at TIMESTAMP DEFAULT NOW()
260
- )""",
261
- """CREATE TABLE IF NOT EXISTS posts (
262
- id SERIAL PRIMARY KEY,
263
- user_id INTEGER REFERENCES users(id),
264
- title VARCHAR,
265
- content TEXT,
266
- post_type VARCHAR,
267
- product_category VARCHAR,
268
- scheduled_date TIMESTAMP,
269
- status VARCHAR,
270
- linkedin_post_id VARCHAR,
271
- canva_design_id VARCHAR,
272
- assets JSONB,
273
- extra_metadata JSONB,
274
- created_at TIMESTAMP DEFAULT NOW(),
275
- updated_at TIMESTAMP DEFAULT NOW()
276
- )""",
277
- """CREATE TABLE IF NOT EXISTS campaigns (
278
- id SERIAL PRIMARY KEY,
279
- user_id INTEGER REFERENCES users(id),
280
- name VARCHAR,
281
- date_range_start TIMESTAMP,
282
- date_range_end TIMESTAMP,
283
- products JSONB,
284
- post_types JSONB,
285
- posts_per_week INTEGER,
286
- status VARCHAR,
287
- created_at TIMESTAMP DEFAULT NOW(),
288
- updated_at TIMESTAMP DEFAULT NOW()
289
- )"""
290
- ]
291
- for sql in tables_sql:
292
- cursor.execute(sql)
293
- conn.commit()
294
-
295
- # Add new columns to assets table if they don't exist (migration)
296
- # CockroachDB doesn't support ALTER TABLE in DO blocks, so we check first
297
- try:
298
- cursor = conn.cursor()
299
-
300
- # Check if columns exist and add them if they don't
301
- cursor.execute("""
302
- SELECT column_name
303
- FROM information_schema.columns
304
- WHERE table_name='assets' AND column_name='extracted_content'
305
- """)
306
- if not cursor.fetchone():
307
- cursor.execute("ALTER TABLE assets ADD COLUMN extracted_content JSONB")
308
- print("✓ Added extracted_content column")
309
-
310
- cursor.execute("""
311
- SELECT column_name
312
- FROM information_schema.columns
313
- WHERE table_name='assets' AND column_name='analysis_status'
314
- """)
315
- if not cursor.fetchone():
316
- cursor.execute("ALTER TABLE assets ADD COLUMN analysis_status VARCHAR DEFAULT 'pending'")
317
- print("✓ Added analysis_status column")
318
-
319
- cursor.execute("""
320
- SELECT column_name
321
- FROM information_schema.columns
322
- WHERE table_name='assets' AND column_name='analyzed_at'
323
- """)
324
- if not cursor.fetchone():
325
- cursor.execute("ALTER TABLE assets ADD COLUMN analyzed_at TIMESTAMP")
326
- print("✓ Added analyzed_at column")
327
-
328
- conn.commit()
329
- cursor.close()
330
- print("✓ Database migration completed (added new asset columns)")
331
- except Exception as migration_error:
332
- # Migration might fail if columns already exist, that's okay
333
- print(f"Migration note: {migration_error}")
334
-
335
- conn.close()
336
- print("✓ CockroachDB tables created successfully (using direct psycopg2 connection)")
337
- return True
338
- except Exception as raw_error:
339
- print(f" Table creation failed: {raw_error}")
340
- print("✓ Database connection works - tables will be created on first use")
341
- return True # Connection works, tables will be created later
342
- else:
343
- # Real error, not just version parsing
344
- raise create_error
345
- except Exception as e:
346
- error_str = str(e)
347
- if "Could not determine version" in error_str:
348
- print("⚠ CockroachDB version parsing issue (non-fatal)")
349
- print("✓ Database connection works - tables will be created on first use")
350
- return True # Connection works, return True
351
- else:
352
- print(f"Database connection failed: {e}")
353
- return False
354
-
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, text
3
+ from sqlalchemy.orm import sessionmaker, declarative_base
4
+ from sqlalchemy.pool import NullPool
5
+
6
+ # Try to use CockroachDB dialect if available
7
+ try:
8
+ import cockroachdb.sqlalchemy.dialect
9
+ COCKROACHDB_AVAILABLE = True
10
+ except ImportError:
11
+ COCKROACHDB_AVAILABLE = False
12
+
13
+ # Get database URL from environment variable
14
+ # Default to SQLite for local development if not set
15
+ ORIGINAL_DATABASE_URL = os.getenv(
16
+ "DATABASE_URL",
17
+ "sqlite:///./postgen.db"
18
+ )
19
+ DATABASE_URL = ORIGINAL_DATABASE_URL
20
+
21
+ # For CockroachDB, we need to handle SSL and connection pooling
22
+ if ORIGINAL_DATABASE_URL.startswith("postgresql://") or ORIGINAL_DATABASE_URL.startswith("postgres://") or ORIGINAL_DATABASE_URL.startswith("cockroachdb://"):
23
+ # Check if this is a CockroachDB connection (use original URL before modifications)
24
+ is_cockroach = "cockroachlabs" in ORIGINAL_DATABASE_URL.lower()
25
+
26
+ # CockroachDB connection - use NullPool to avoid connection issues
27
+ # CockroachDB requires SSL, so we ensure sslmode is set
28
+ # Use 'require' mode which uses SSL but doesn't require certificate file
29
+ # For production with certificate, use 'verify-full' and provide sslrootcert
30
+ cert_path = os.path.expanduser("~/.postgresql/root.crt")
31
+
32
+ if "sslmode" not in DATABASE_URL:
33
+ separator = "&" if "?" in DATABASE_URL else "?"
34
+ # Use 'require' instead of 'verify-full' to work without certificate file
35
+ # Still secure (uses SSL) but doesn't verify the certificate
36
+ DATABASE_URL = f"{DATABASE_URL}{separator}sslmode=require"
37
+ elif "sslmode=verify-full" in DATABASE_URL and not os.path.exists(cert_path):
38
+ # If verify-full is set but cert file doesn't exist, change to require
39
+ DATABASE_URL = DATABASE_URL.replace("sslmode=verify-full", "sslmode=require")
40
+ print("⚠ Certificate file not found, using sslmode=require instead of verify-full")
41
+
42
+ # Use CockroachDB dialect if available and this is a CockroachDB connection
43
+ if is_cockroach and COCKROACHDB_AVAILABLE:
44
+ # Replace postgresql:// with cockroachdb:// to use CockroachDB dialect
45
+ DATABASE_URL = DATABASE_URL.replace("postgresql://", "cockroachdb://", 1)
46
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "cockroachdb://", 1)
47
+
48
+ # Configure engine
49
+ engine = create_engine(
50
+ DATABASE_URL,
51
+ poolclass=NullPool, # CockroachDB works better with NullPool
52
+ echo=False, # Set to True for SQL query debugging
53
+ connect_args={} # No special connect args needed
54
+ )
55
+ else:
56
+ # SQLite for local development
57
+ engine = create_engine(
58
+ DATABASE_URL,
59
+ connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
60
+ )
61
+
62
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
63
+
64
+ Base = declarative_base()
65
+
66
+ def get_db():
67
+ """Dependency to get database session"""
68
+ db = SessionLocal()
69
+ try:
70
+ yield db
71
+ finally:
72
+ db.close()
73
+
74
+ def get_direct_psycopg2_connection():
75
+ """Get a direct psycopg2 connection bypassing SQLAlchemy version parsing"""
76
+ try:
77
+ import psycopg2
78
+ from urllib.parse import urlparse, parse_qs
79
+
80
+ # Only for PostgreSQL/CockroachDB connections
81
+ if not (ORIGINAL_DATABASE_URL.startswith("postgresql://") or
82
+ ORIGINAL_DATABASE_URL.startswith("postgres://") or
83
+ ORIGINAL_DATABASE_URL.startswith("cockroachdb://")):
84
+ return None
85
+
86
+ # Parse connection string
87
+ url_for_parsing = ORIGINAL_DATABASE_URL.replace("cockroachdb://", "postgresql://")
88
+ parsed = urlparse(url_for_parsing)
89
+ dbname = parsed.path[1:] if parsed.path else "defaultdb"
90
+ user = parsed.username
91
+ password = parsed.password
92
+ host = parsed.hostname
93
+ port = parsed.port or 26257
94
+
95
+ # Get sslmode from query params
96
+ params = parse_qs(parsed.query)
97
+ sslmode_list = params.get('sslmode', ['require'])
98
+ sslmode = sslmode_list[0] if sslmode_list else 'require'
99
+
100
+ # Check if certificate file exists for verify-full mode
101
+ cert_path = os.path.expanduser("~/.postgresql/root.crt")
102
+ if sslmode == 'verify-full' and not os.path.exists(cert_path):
103
+ # Change to require mode if certificate doesn't exist
104
+ sslmode = 'require'
105
+
106
+ # Create connection
107
+ conn = psycopg2.connect(
108
+ dbname=dbname,
109
+ user=user,
110
+ password=password,
111
+ host=host,
112
+ port=port,
113
+ sslmode=sslmode
114
+ )
115
+ return conn
116
+ except Exception as e:
117
+ print(f"Failed to create direct psycopg2 connection: {e}")
118
+ return None
119
+
120
+ def ensure_default_user():
121
+ """Ensure a default user (id=1) exists in the database"""
122
+ try:
123
+ conn = get_direct_psycopg2_connection()
124
+ if not conn:
125
+ return 1 # Return default ID if connection fails
126
+
127
+ try:
128
+ cursor = conn.cursor()
129
+ # Check if user with id=1 exists
130
+ cursor.execute("SELECT id FROM users WHERE id = 1")
131
+ if cursor.fetchone():
132
+ cursor.close()
133
+ conn.close()
134
+ return 1
135
+
136
+ # Create default user if it doesn't exist
137
+ # Try with ON CONFLICT first (PostgreSQL/CockroachDB)
138
+ try:
139
+ cursor.execute("""
140
+ INSERT INTO users (id, email, name, created_at)
141
+ VALUES (1, 'default@postgen.app', 'Default User', NOW())
142
+ ON CONFLICT (id) DO NOTHING
143
+ """)
144
+ except Exception:
145
+ # If ON CONFLICT fails, try without it (might be unique constraint on email)
146
+ try:
147
+ cursor.execute("""
148
+ INSERT INTO users (id, email, name, created_at)
149
+ VALUES (1, 'default@postgen.app', 'Default User', NOW())
150
+ """)
151
+ except Exception as insert_error:
152
+ # User might already exist (race condition), check again
153
+ cursor.execute("SELECT id FROM users WHERE id = 1 OR email = 'default@postgen.app' LIMIT 1")
154
+ row = cursor.fetchone()
155
+ if row:
156
+ cursor.close()
157
+ conn.close()
158
+ return row[0]
159
+ # If still fails, re-raise
160
+ raise insert_error
161
+
162
+ conn.commit()
163
+ cursor.close()
164
+ conn.close()
165
+ return 1
166
+ except Exception as e:
167
+ # If everything fails, try to get any existing user or return default
168
+ try:
169
+ cursor.execute("SELECT id FROM users LIMIT 1")
170
+ row = cursor.fetchone()
171
+ cursor.close()
172
+ conn.close()
173
+ if row:
174
+ return row[0]
175
+ except:
176
+ if conn:
177
+ conn.close()
178
+ print(f"Warning: Could not ensure default user: {e}")
179
+ return 1 # Return default ID as fallback
180
+ except Exception as e:
181
+ print(f"Error ensuring default user: {e}")
182
+ return 1 # Return default ID as fallback
183
+
184
+ def init_db():
185
+ """Initialize database tables"""
186
+ try:
187
+ from app.models import User, Integration, Asset, Post, Campaign
188
+
189
+ # Try to create tables
190
+ # For CockroachDB, version parsing may fail but connection still works
191
+ try:
192
+ Base.metadata.create_all(bind=engine)
193
+ print("✓ Database tables created successfully")
194
+ return True
195
+ except Exception as create_error:
196
+ error_str = str(create_error)
197
+ # Check if it's a version parsing error (non-fatal for CockroachDB)
198
+ if "Could not determine version" in error_str:
199
+ # Version parsing failed, but CockroachDB connection works
200
+ # Use psycopg2 directly to bypass SQLAlchemy's version parsing
201
+ try:
202
+ import psycopg2
203
+ from urllib.parse import urlparse, parse_qs
204
+
205
+ # Parse connection string to get connection parameters
206
+ # Handle both cockroachdb:// and postgresql:// schemes
207
+ # Use original URL before any modifications
208
+ url_for_parsing = ORIGINAL_DATABASE_URL.replace("cockroachdb://", "postgresql://")
209
+ parsed = urlparse(url_for_parsing)
210
+ dbname = parsed.path[1:] if parsed.path else "defaultdb"
211
+ user = parsed.username
212
+ password = parsed.password
213
+ host = parsed.hostname
214
+ port = parsed.port or 26257
215
+
216
+ # Get sslmode from query params (use require as default for CockroachDB)
217
+ params = parse_qs(parsed.query)
218
+ sslmode_list = params.get('sslmode', ['require'])
219
+ sslmode = sslmode_list[0] if sslmode_list else 'require'
220
+
221
+ # Connect directly with psycopg2 (bypasses SQLAlchemy version parsing)
222
+ conn = psycopg2.connect(
223
+ dbname=dbname,
224
+ user=user,
225
+ password=password,
226
+ host=host,
227
+ port=port,
228
+ sslmode=sslmode
229
+ )
230
+
231
+ cursor = conn.cursor()
232
+ # Create tables using IF NOT EXISTS
233
+ tables_sql = [
234
+ """CREATE TABLE IF NOT EXISTS users (
235
+ id SERIAL PRIMARY KEY,
236
+ email VARCHAR UNIQUE,
237
+ name VARCHAR,
238
+ created_at TIMESTAMP DEFAULT NOW()
239
+ )""",
240
+ """CREATE TABLE IF NOT EXISTS integrations (
241
+ id SERIAL PRIMARY KEY,
242
+ user_id INTEGER REFERENCES users(id),
243
+ provider VARCHAR,
244
+ access_token TEXT,
245
+ refresh_token TEXT,
246
+ expires_at TIMESTAMP,
247
+ account_info JSONB,
248
+ connected BOOLEAN DEFAULT FALSE,
249
+ created_at TIMESTAMP DEFAULT NOW(),
250
+ updated_at TIMESTAMP DEFAULT NOW()
251
+ )""",
252
+ """CREATE TABLE IF NOT EXISTS assets (
253
+ id SERIAL PRIMARY KEY,
254
+ user_id INTEGER REFERENCES users(id),
255
+ name VARCHAR,
256
+ file_path VARCHAR,
257
+ file_type VARCHAR,
258
+ product_category VARCHAR,
259
+ sub_category VARCHAR,
260
+ size INTEGER,
261
+ extra_metadata JSONB,
262
+ extracted_content JSONB,
263
+ analysis_status VARCHAR DEFAULT 'pending',
264
+ analyzed_at TIMESTAMP,
265
+ created_at TIMESTAMP DEFAULT NOW()
266
+ )""",
267
+ """CREATE TABLE IF NOT EXISTS posts (
268
+ id SERIAL PRIMARY KEY,
269
+ user_id INTEGER REFERENCES users(id),
270
+ title VARCHAR,
271
+ content TEXT,
272
+ post_type VARCHAR,
273
+ product_category VARCHAR,
274
+ scheduled_date TIMESTAMP,
275
+ status VARCHAR,
276
+ linkedin_post_id VARCHAR,
277
+ canva_design_id VARCHAR,
278
+ assets JSONB,
279
+ extra_metadata JSONB,
280
+ created_at TIMESTAMP DEFAULT NOW(),
281
+ updated_at TIMESTAMP DEFAULT NOW()
282
+ )""",
283
+ """CREATE TABLE IF NOT EXISTS campaigns (
284
+ id SERIAL PRIMARY KEY,
285
+ user_id INTEGER REFERENCES users(id),
286
+ name VARCHAR,
287
+ date_range_start TIMESTAMP,
288
+ date_range_end TIMESTAMP,
289
+ products JSONB,
290
+ post_types JSONB,
291
+ posts_per_week INTEGER,
292
+ status VARCHAR,
293
+ created_at TIMESTAMP DEFAULT NOW(),
294
+ updated_at TIMESTAMP DEFAULT NOW()
295
+ )"""
296
+ ]
297
+ for sql in tables_sql:
298
+ cursor.execute(sql)
299
+ conn.commit()
300
+
301
+ # Add new columns to assets table if they don't exist (migration)
302
+ # CockroachDB doesn't support ALTER TABLE in DO blocks, so we check first
303
+ try:
304
+ cursor = conn.cursor()
305
+
306
+ # Check if columns exist and add them if they don't
307
+ cursor.execute("""
308
+ SELECT column_name
309
+ FROM information_schema.columns
310
+ WHERE table_name='assets' AND column_name='extracted_content'
311
+ """)
312
+ if not cursor.fetchone():
313
+ cursor.execute("ALTER TABLE assets ADD COLUMN extracted_content JSONB")
314
+ print("✓ Added extracted_content column")
315
+
316
+ cursor.execute("""
317
+ SELECT column_name
318
+ FROM information_schema.columns
319
+ WHERE table_name='assets' AND column_name='analysis_status'
320
+ """)
321
+ if not cursor.fetchone():
322
+ cursor.execute("ALTER TABLE assets ADD COLUMN analysis_status VARCHAR DEFAULT 'pending'")
323
+ print("✓ Added analysis_status column")
324
+
325
+ cursor.execute("""
326
+ SELECT column_name
327
+ FROM information_schema.columns
328
+ WHERE table_name='assets' AND column_name='analyzed_at'
329
+ """)
330
+ if not cursor.fetchone():
331
+ cursor.execute("ALTER TABLE assets ADD COLUMN analyzed_at TIMESTAMP")
332
+ print("✓ Added analyzed_at column")
333
+
334
+ conn.commit()
335
+ cursor.close()
336
+ print("✓ Database migration completed (added new asset columns)")
337
+ except Exception as migration_error:
338
+ # Migration might fail if columns already exist, that's okay
339
+ print(f"Migration note: {migration_error}")
340
+
341
+ conn.close()
342
+ print("✓ CockroachDB tables created successfully (using direct psycopg2 connection)")
343
+ return True
344
+ except Exception as raw_error:
345
+ print(f"⚠ Table creation failed: {raw_error}")
346
+ print("✓ Database connection works - tables will be created on first use")
347
+ return True # Connection works, tables will be created later
348
+ else:
349
+ # Real error, not just version parsing
350
+ raise create_error
351
+ except Exception as e:
352
+ error_str = str(e)
353
+ if "Could not determine version" in error_str:
354
+ print("⚠ CockroachDB version parsing issue (non-fatal)")
355
+ print("✓ Database connection works - tables will be created on first use")
356
+ return True # Connection works, return True
357
+ else:
358
+ print(f"Database connection failed: {e}")
359
+ return False
360
+
backend/app/main.py CHANGED
The diff for this file is too large to render. See raw diff
 
backend/app/schemas.py CHANGED
@@ -1,118 +1,117 @@
1
- from pydantic import BaseModel, EmailStr
2
- from datetime import datetime
3
- from typing import Optional, List, Dict, Any
4
-
5
- class IntegrationCreate(BaseModel):
6
- provider: str
7
- access_token: str
8
- refresh_token: Optional[str] = None
9
- expires_at: Optional[datetime] = None
10
- account_info: Optional[Dict[str, Any]] = None
11
-
12
- class IntegrationResponse(BaseModel):
13
- id: int
14
- provider: str
15
- connected: bool
16
- account_info: Optional[Dict[str, Any]] = None
17
- created_at: datetime
18
-
19
- class Config:
20
- from_attributes = True
21
-
22
- class AssetCreate(BaseModel):
23
- name: str
24
- file_type: str
25
- product_category: str
26
- sub_category: Optional[str] = None
27
- size: int
28
- metadata: Optional[Dict[str, Any]] = None
29
-
30
- class AssetResponse(BaseModel):
31
- id: int
32
- name: str
33
- file_type: str
34
- product_category: str
35
- sub_category: Optional[str] = None
36
- size: int
37
- extracted_content: Optional[Dict[str, Any]] = None
38
- analysis_status: Optional[str] = None
39
- analyzed_at: Optional[datetime] = None
40
- created_at: datetime
41
-
42
- class Config:
43
- from_attributes = True
44
-
45
- class PostCreate(BaseModel):
46
- title: str
47
- content: str
48
- post_type: str
49
- product_category: str
50
- scheduled_date: datetime
51
- assets: Optional[List[int]] = None
52
-
53
- class PostResponse(BaseModel):
54
- id: int
55
- title: str
56
- content: str
57
- post_type: str
58
- product_category: str
59
- scheduled_date: datetime
60
- status: str
61
- created_at: datetime
62
-
63
- class Config:
64
- from_attributes = True
65
-
66
- class CampaignCreate(BaseModel):
67
- name: str
68
- date_range_start: datetime
69
- date_range_end: datetime
70
- products: List[str]
71
- post_types: List[str]
72
- posts_per_week: int
73
-
74
- class CampaignResponse(BaseModel):
75
- id: int
76
- name: str
77
- date_range_start: datetime
78
- date_range_end: datetime
79
- products: List[str]
80
- post_types: List[str]
81
- posts_per_week: int
82
- status: str
83
- created_at: datetime
84
-
85
- class Config:
86
- from_attributes = True
87
-
88
- class CanvaBrandTemplate(BaseModel):
89
- id: str
90
- title: str
91
- view_url: str
92
- create_url: str
93
- thumbnail: Optional[Dict[str, Any]] = None
94
-
95
- class CanvaAutofillRequest(BaseModel):
96
- brand_template_id: str
97
- title: str
98
- data: Dict[str, Any]
99
-
100
- class CanvaAutofillResponse(BaseModel):
101
- job_id: str
102
- status: str
103
-
104
- class LinkedInPostRequest(BaseModel):
105
- text: str
106
- media_uris: Optional[List[str]] = None
107
- scheduled_time: Optional[datetime] = None
108
-
109
- class AIContentRequest(BaseModel):
110
- product_category: str
111
- post_type: str
112
- context: Optional[str] = None
113
- assets: Optional[List[int]] = None
114
-
115
- class AIContentResponse(BaseModel):
116
- content: str
117
- suggested_hashtags: List[str]
118
-
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from datetime import datetime
3
+ from typing import Optional, List, Dict, Any
4
+
5
+ class IntegrationCreate(BaseModel):
6
+ provider: str
7
+ access_token: str
8
+ refresh_token: Optional[str] = None
9
+ expires_at: Optional[datetime] = None
10
+ account_info: Optional[Dict[str, Any]] = None
11
+
12
+ class IntegrationResponse(BaseModel):
13
+ id: int
14
+ provider: str
15
+ connected: bool
16
+ account_info: Optional[Dict[str, Any]] = None
17
+ created_at: datetime
18
+
19
+ class Config:
20
+ from_attributes = True
21
+
22
+ class AssetCreate(BaseModel):
23
+ name: str
24
+ file_type: str
25
+ product_category: str
26
+ sub_category: Optional[str] = None
27
+ size: int
28
+ metadata: Optional[Dict[str, Any]] = None
29
+
30
+ class AssetResponse(BaseModel):
31
+ id: str # Changed to str to preserve precision for large CockroachDB IDs (bigint)
32
+ name: str
33
+ file_type: str
34
+ product_category: str
35
+ sub_category: Optional[str] = None
36
+ size: int
37
+ extracted_content: Optional[Dict[str, Any]] = None
38
+ analysis_status: Optional[str] = None
39
+ analyzed_at: Optional[datetime] = None
40
+ created_at: datetime
41
+
42
+ class Config:
43
+ from_attributes = True
44
+
45
+ class PostCreate(BaseModel):
46
+ title: str
47
+ content: str
48
+ post_type: str
49
+ product_category: str
50
+ scheduled_date: datetime
51
+ assets: Optional[List[int]] = None
52
+
53
+ class PostResponse(BaseModel):
54
+ id: int
55
+ title: str
56
+ content: str
57
+ post_type: str
58
+ product_category: str
59
+ scheduled_date: datetime
60
+ status: str
61
+ created_at: datetime
62
+
63
+ class Config:
64
+ from_attributes = True
65
+
66
+ class CampaignCreate(BaseModel):
67
+ name: str
68
+ date_range_start: datetime
69
+ date_range_end: datetime
70
+ products: List[str]
71
+ post_types: List[str]
72
+ posts_per_week: int
73
+
74
+ class CampaignResponse(BaseModel):
75
+ id: int
76
+ name: str
77
+ date_range_start: datetime
78
+ date_range_end: datetime
79
+ products: List[str]
80
+ post_types: List[str]
81
+ posts_per_week: int
82
+ status: str
83
+ created_at: datetime
84
+
85
+ class Config:
86
+ from_attributes = True
87
+
88
+ class CanvaBrandTemplate(BaseModel):
89
+ id: str
90
+ title: str
91
+ view_url: str
92
+ create_url: str
93
+ thumbnail: Optional[Dict[str, Any]] = None
94
+
95
+ class CanvaAutofillRequest(BaseModel):
96
+ brand_template_id: str
97
+ title: str
98
+ data: Dict[str, Any]
99
+
100
+ class CanvaAutofillResponse(BaseModel):
101
+ job_id: str
102
+ status: str
103
+
104
+ class LinkedInPostRequest(BaseModel):
105
+ text: str
106
+ media_uris: Optional[List[str]] = None
107
+ scheduled_time: Optional[datetime] = None
108
+
109
+ class AIContentRequest(BaseModel):
110
+ product_category: str
111
+ post_type: str
112
+ context: Optional[str] = None
113
+ assets: Optional[List[int]] = None
114
+
115
+ class AIContentResponse(BaseModel):
116
+ content: str
117
+ suggested_hashtags: List[str]
 
frontend/.DS_Store ADDED
Binary file (8.2 kB). View file
 
frontend/src/.DS_Store ADDED
Binary file (10.2 kB). View file
 
frontend/src/components/.DS_Store ADDED
Binary file (8.2 kB). View file
 
frontend/src/pages/Repository.jsx CHANGED
The diff for this file is too large to render. See raw diff