mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: I think namespaces might of messed up the schema - It could detect this and wipe the slate clean. Update IndexedDB and Flask.
This commit is contained in:
259
backend/app.py
259
backend/app.py
@@ -30,26 +30,105 @@ def get_db():
|
|||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
def check_and_migrate_schema():
|
||||||
|
"""Check if schema needs migration and perform it if necessary"""
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
|
||||||
|
namespaces_exists = cursor.fetchone() is not None
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(snippets)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
has_namespace_id = 'namespaceId' in columns
|
||||||
|
|
||||||
|
if not namespaces_exists or not has_namespace_id:
|
||||||
|
print("Schema migration needed - recreating tables with namespace support...")
|
||||||
|
|
||||||
|
cursor.execute("DROP TABLE IF EXISTS snippets")
|
||||||
|
cursor.execute("DROP TABLE IF EXISTS namespaces")
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE namespaces (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
createdAt INTEGER NOT NULL,
|
||||||
|
isDefault INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE snippets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
language TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
namespaceId TEXT,
|
||||||
|
hasPreview INTEGER DEFAULT 0,
|
||||||
|
functionName TEXT,
|
||||||
|
inputParameters TEXT,
|
||||||
|
createdAt INTEGER NOT NULL,
|
||||||
|
updatedAt INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||||
|
VALUES ('default', 'Default', ?, 1)
|
||||||
|
''', (int(datetime.utcnow().timestamp() * 1000),))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("Schema migration completed")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS namespaces (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
createdAt INTEGER NOT NULL,
|
||||||
|
isDefault INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS snippets (
|
CREATE TABLE IF NOT EXISTS snippets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
code TEXT NOT NULL,
|
code TEXT NOT NULL,
|
||||||
language TEXT NOT NULL,
|
language TEXT NOT NULL,
|
||||||
description TEXT,
|
category TEXT NOT NULL,
|
||||||
tags TEXT,
|
namespaceId TEXT,
|
||||||
category TEXT DEFAULT 'general',
|
hasPreview INTEGER DEFAULT 0,
|
||||||
componentName TEXT,
|
functionName TEXT,
|
||||||
previewParams TEXT,
|
inputParameters TEXT,
|
||||||
createdAt TEXT NOT NULL,
|
createdAt INTEGER NOT NULL,
|
||||||
updatedAt TEXT NOT NULL
|
updatedAt INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM namespaces WHERE isDefault = 1")
|
||||||
|
default_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if default_count == 0:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||||
|
VALUES ('default', 'Default', ?, 1)
|
||||||
|
''', (int(datetime.utcnow().timestamp() * 1000),))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
check_and_migrate_schema()
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
def health():
|
def health():
|
||||||
@@ -67,10 +146,12 @@ def get_snippets():
|
|||||||
snippets = []
|
snippets = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
snippet = dict(row)
|
snippet = dict(row)
|
||||||
if snippet.get('tags'):
|
if snippet.get('inputParameters'):
|
||||||
snippet['tags'] = json.loads(snippet['tags'])
|
try:
|
||||||
if snippet.get('previewParams'):
|
snippet['inputParameters'] = json.loads(snippet['inputParameters'])
|
||||||
snippet['previewParams'] = json.loads(snippet['previewParams'])
|
except:
|
||||||
|
snippet['inputParameters'] = None
|
||||||
|
snippet['hasPreview'] = bool(snippet.get('hasPreview', 0))
|
||||||
snippets.append(snippet)
|
snippets.append(snippet)
|
||||||
|
|
||||||
return jsonify(snippets)
|
return jsonify(snippets)
|
||||||
@@ -90,10 +171,12 @@ def get_snippet(snippet_id):
|
|||||||
return jsonify({'error': 'Snippet not found'}), 404
|
return jsonify({'error': 'Snippet not found'}), 404
|
||||||
|
|
||||||
snippet = dict(row)
|
snippet = dict(row)
|
||||||
if snippet.get('tags'):
|
if snippet.get('inputParameters'):
|
||||||
snippet['tags'] = json.loads(snippet['tags'])
|
try:
|
||||||
if snippet.get('previewParams'):
|
snippet['inputParameters'] = json.loads(snippet['inputParameters'])
|
||||||
snippet['previewParams'] = json.loads(snippet['previewParams'])
|
except:
|
||||||
|
snippet['inputParameters'] = None
|
||||||
|
snippet['hasPreview'] = bool(snippet.get('hasPreview', 0))
|
||||||
|
|
||||||
return jsonify(snippet)
|
return jsonify(snippet)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -106,24 +189,32 @@ def create_snippet():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
tags_json = json.dumps(data.get('tags', []))
|
input_params_json = json.dumps(data.get('inputParameters')) if data.get('inputParameters') else None
|
||||||
preview_params_json = json.dumps(data.get('previewParams', {}))
|
|
||||||
|
created_at = data.get('createdAt')
|
||||||
|
if isinstance(created_at, str):
|
||||||
|
created_at = int(datetime.fromisoformat(created_at.replace('Z', '+00:00')).timestamp() * 1000)
|
||||||
|
|
||||||
|
updated_at = data.get('updatedAt')
|
||||||
|
if isinstance(updated_at, str):
|
||||||
|
updated_at = int(datetime.fromisoformat(updated_at.replace('Z', '+00:00')).timestamp() * 1000)
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO snippets (id, title, code, language, description, tags, category, componentName, previewParams, createdAt, updatedAt)
|
INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
data['id'],
|
data['id'],
|
||||||
data['title'],
|
data['title'],
|
||||||
|
data.get('description', ''),
|
||||||
data['code'],
|
data['code'],
|
||||||
data['language'],
|
data['language'],
|
||||||
data.get('description', ''),
|
|
||||||
tags_json,
|
|
||||||
data.get('category', 'general'),
|
data.get('category', 'general'),
|
||||||
data.get('componentName', ''),
|
data.get('namespaceId'),
|
||||||
preview_params_json,
|
1 if data.get('hasPreview') else 0,
|
||||||
data['createdAt'],
|
data.get('functionName'),
|
||||||
data['updatedAt']
|
input_params_json,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
))
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -140,23 +231,27 @@ def update_snippet(snippet_id):
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
tags_json = json.dumps(data.get('tags', []))
|
input_params_json = json.dumps(data.get('inputParameters')) if data.get('inputParameters') else None
|
||||||
preview_params_json = json.dumps(data.get('previewParams', {}))
|
|
||||||
|
updated_at = data.get('updatedAt')
|
||||||
|
if isinstance(updated_at, str):
|
||||||
|
updated_at = int(datetime.fromisoformat(updated_at.replace('Z', '+00:00')).timestamp() * 1000)
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
UPDATE snippets
|
UPDATE snippets
|
||||||
SET title = ?, code = ?, language = ?, description = ?, tags = ?, category = ?, componentName = ?, previewParams = ?, updatedAt = ?
|
SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (
|
''', (
|
||||||
data['title'],
|
data['title'],
|
||||||
|
data.get('description', ''),
|
||||||
data['code'],
|
data['code'],
|
||||||
data['language'],
|
data['language'],
|
||||||
data.get('description', ''),
|
|
||||||
tags_json,
|
|
||||||
data.get('category', 'general'),
|
data.get('category', 'general'),
|
||||||
data.get('componentName', ''),
|
data.get('namespaceId'),
|
||||||
preview_params_json,
|
1 if data.get('hasPreview') else 0,
|
||||||
data['updatedAt'],
|
data.get('functionName'),
|
||||||
|
input_params_json,
|
||||||
|
updated_at,
|
||||||
snippet_id
|
snippet_id
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -186,6 +281,104 @@ def delete_snippet(snippet_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/namespaces', methods=['GET'])
|
||||||
|
def get_namespaces():
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
namespaces = []
|
||||||
|
for row in rows:
|
||||||
|
namespace = dict(row)
|
||||||
|
namespace['isDefault'] = bool(namespace.get('isDefault', 0))
|
||||||
|
namespaces.append(namespace)
|
||||||
|
|
||||||
|
return jsonify(namespaces)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/namespaces', methods=['POST'])
|
||||||
|
def create_namespace():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
created_at = data.get('createdAt')
|
||||||
|
if isinstance(created_at, str):
|
||||||
|
created_at = int(datetime.fromisoformat(created_at.replace('Z', '+00:00')).timestamp() * 1000)
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
data['id'],
|
||||||
|
data['name'],
|
||||||
|
created_at,
|
||||||
|
1 if data.get('isDefault') else 0
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(data), 201
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/namespaces/<namespace_id>', methods=['DELETE'])
|
||||||
|
def delete_namespace(namespace_id):
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('SELECT isDefault FROM namespaces WHERE id = ?', (namespace_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'error': 'Namespace not found'}), 404
|
||||||
|
|
||||||
|
if row['isDefault']:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'error': 'Cannot delete default namespace'}), 400
|
||||||
|
|
||||||
|
cursor.execute('SELECT id FROM namespaces WHERE isDefault = 1')
|
||||||
|
default_row = cursor.fetchone()
|
||||||
|
default_id = default_row['id'] if default_row else 'default'
|
||||||
|
|
||||||
|
cursor.execute('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', (default_id, namespace_id))
|
||||||
|
|
||||||
|
cursor.execute('DELETE FROM namespaces WHERE id = ?', (namespace_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/wipe', methods=['POST'])
|
||||||
|
def wipe_database():
|
||||||
|
"""Emergency endpoint to wipe and recreate the database"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("DROP TABLE IF EXISTS snippets")
|
||||||
|
cursor.execute("DROP TABLE IF EXISTS namespaces")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'message': 'Database wiped and recreated'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_db()
|
init_db()
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
|
|||||||
131
src/lib/db.ts
131
src/lib/db.ts
@@ -112,6 +112,64 @@ function saveToLocalStorage(data: Uint8Array): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateSchema(db: Database): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||||
|
if (snippetsCheck.length === 0) return true
|
||||||
|
|
||||||
|
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||||
|
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||||
|
|
||||||
|
for (const col of requiredColumns) {
|
||||||
|
if (!columns.includes(col)) {
|
||||||
|
console.warn(`Schema validation failed: missing column '${col}'`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespacesCheck = db.exec("PRAGMA table_info(namespaces)")
|
||||||
|
if (namespacesCheck.length === 0) {
|
||||||
|
console.warn('Schema validation failed: namespaces table missing')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Schema validation error:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wipeAndRecreateDB(): Promise<void> {
|
||||||
|
console.warn('Wiping corrupted database and creating fresh schema...')
|
||||||
|
|
||||||
|
await saveToIndexedDB(new Uint8Array())
|
||||||
|
saveToLocalStorage(new Uint8Array())
|
||||||
|
|
||||||
|
const idb = await openIndexedDB()
|
||||||
|
if (idb) {
|
||||||
|
try {
|
||||||
|
const transaction = idb.transaction([IDB_STORE], 'readwrite')
|
||||||
|
const store = transaction.objectStore(IDB_STORE)
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const request = store.delete(DB_KEY)
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
request.onerror = () => resolve()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error clearing IndexedDB:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(DB_KEY)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error clearing localStorage:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbInstance = null
|
||||||
|
}
|
||||||
|
|
||||||
export async function initDB(): Promise<Database> {
|
export async function initDB(): Promise<Database> {
|
||||||
if (dbInstance) return dbInstance
|
if (dbInstance) return dbInstance
|
||||||
|
|
||||||
@@ -122,6 +180,7 @@ export async function initDB(): Promise<Database> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let loadedData: Uint8Array | null = null
|
let loadedData: Uint8Array | null = null
|
||||||
|
let schemaValid = false
|
||||||
|
|
||||||
loadedData = await loadFromIndexedDB()
|
loadedData = await loadFromIndexedDB()
|
||||||
|
|
||||||
@@ -129,11 +188,22 @@ export async function initDB(): Promise<Database> {
|
|||||||
loadedData = loadFromLocalStorage()
|
loadedData = loadFromLocalStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadedData) {
|
if (loadedData && loadedData.length > 0) {
|
||||||
try {
|
try {
|
||||||
dbInstance = new sqlInstance.Database(loadedData)
|
const testDb = new sqlInstance.Database(loadedData)
|
||||||
|
schemaValid = await validateSchema(testDb)
|
||||||
|
|
||||||
|
if (schemaValid) {
|
||||||
|
dbInstance = testDb
|
||||||
|
} else {
|
||||||
|
console.warn('Schema validation failed, wiping database')
|
||||||
|
testDb.close()
|
||||||
|
await wipeAndRecreateDB()
|
||||||
|
dbInstance = new sqlInstance.Database()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load saved database, creating new one:', error)
|
console.error('Failed to load saved database, creating new one:', error)
|
||||||
|
await wipeAndRecreateDB()
|
||||||
dbInstance = new sqlInstance.Database()
|
dbInstance = new sqlInstance.Database()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -836,6 +906,12 @@ export async function getDatabaseStats(): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clearDatabase(): Promise<void> {
|
export async function clearDatabase(): Promise<void> {
|
||||||
|
const adapter = getFlaskAdapter()
|
||||||
|
if (adapter) {
|
||||||
|
await adapter.wipeDatabase()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const db = await openIndexedDB()
|
const db = await openIndexedDB()
|
||||||
if (db) {
|
if (db) {
|
||||||
try {
|
try {
|
||||||
@@ -875,6 +951,11 @@ export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllNamespaces(): Promise<import('./types').Namespace[]> {
|
export async function getAllNamespaces(): Promise<import('./types').Namespace[]> {
|
||||||
|
const adapter = getFlaskAdapter()
|
||||||
|
if (adapter) {
|
||||||
|
return await adapter.getAllNamespaces()
|
||||||
|
}
|
||||||
|
|
||||||
const db = await initDB()
|
const db = await initDB()
|
||||||
|
|
||||||
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||||
@@ -898,14 +979,20 @@ export async function getAllNamespaces(): Promise<import('./types').Namespace[]>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
|
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
|
||||||
const db = await initDB()
|
|
||||||
|
|
||||||
const namespace: import('./types').Namespace = {
|
const namespace: import('./types').Namespace = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
name,
|
name,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
isDefault: false
|
isDefault: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adapter = getFlaskAdapter()
|
||||||
|
if (adapter) {
|
||||||
|
await adapter.createNamespace(namespace)
|
||||||
|
return namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await initDB()
|
||||||
|
|
||||||
db.run(
|
db.run(
|
||||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||||
@@ -918,6 +1005,11 @@ export async function createNamespace(name: string): Promise<import('./types').N
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteNamespace(id: string): Promise<void> {
|
export async function deleteNamespace(id: string): Promise<void> {
|
||||||
|
const adapter = getFlaskAdapter()
|
||||||
|
if (adapter) {
|
||||||
|
return await adapter.deleteNamespace(id)
|
||||||
|
}
|
||||||
|
|
||||||
const db = await initDB()
|
const db = await initDB()
|
||||||
|
|
||||||
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
|
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
|
||||||
@@ -1009,3 +1101,34 @@ export async function getNamespaceById(id: string): Promise<import('./types').Na
|
|||||||
|
|
||||||
return namespace
|
return namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function validateDatabaseSchema(): Promise<{ valid: boolean; issues: string[] }> {
|
||||||
|
try {
|
||||||
|
const db = await initDB()
|
||||||
|
const issues: string[] = []
|
||||||
|
|
||||||
|
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||||
|
if (snippetsCheck.length === 0) {
|
||||||
|
issues.push('Snippets table missing')
|
||||||
|
return { valid: false, issues }
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||||
|
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||||
|
|
||||||
|
for (const col of requiredColumns) {
|
||||||
|
if (!columns.includes(col)) {
|
||||||
|
issues.push(`Missing column '${col}' in snippets table`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespacesCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
|
||||||
|
if (namespacesCheck.length === 0) {
|
||||||
|
issues.push('Namespaces table missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: issues.length === 0, issues }
|
||||||
|
} catch (error) {
|
||||||
|
return { valid: false, issues: ['Failed to validate schema: ' + (error as Error).message] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,4 +186,53 @@ export class FlaskStorageAdapter {
|
|||||||
async migrateToIndexedDB(): Promise<Snippet[]> {
|
async migrateToIndexedDB(): Promise<Snippet[]> {
|
||||||
return this.getAllSnippets()
|
return this.getAllSnippets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllNamespaces(): Promise<import('./types').Namespace[]> {
|
||||||
|
if (!this.isValidUrl()) {
|
||||||
|
throw new Error('Invalid Flask backend URL')
|
||||||
|
}
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/namespaces`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch namespaces: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNamespace(namespace: import('./types').Namespace): Promise<void> {
|
||||||
|
if (!this.isValidUrl()) {
|
||||||
|
throw new Error('Invalid Flask backend URL')
|
||||||
|
}
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/namespaces`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(namespace)
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to create namespace: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNamespace(id: string): Promise<void> {
|
||||||
|
if (!this.isValidUrl()) {
|
||||||
|
throw new Error('Invalid Flask backend URL')
|
||||||
|
}
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/namespaces/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to delete namespace: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async wipeDatabase(): Promise<void> {
|
||||||
|
if (!this.isValidUrl()) {
|
||||||
|
throw new Error('Invalid Flask backend URL')
|
||||||
|
}
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/wipe`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to wipe database: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/com
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash } from '@phosphor-icons/react'
|
import { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash, FirstAid, CheckCircle, Warning } from '@phosphor-icons/react'
|
||||||
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets } from '@/lib/db'
|
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets, validateDatabaseSchema } from '@/lib/db'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +30,8 @@ export function SettingsPage() {
|
|||||||
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
|
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
|
||||||
const [testingConnection, setTestingConnection] = useState(false)
|
const [testingConnection, setTestingConnection] = useState(false)
|
||||||
const [envVarSet, setEnvVarSet] = useState(false)
|
const [envVarSet, setEnvVarSet] = useState(false)
|
||||||
|
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
|
||||||
|
const [checkingSchema, setCheckingSchema] = useState(false)
|
||||||
|
|
||||||
const loadStats = async () => {
|
const loadStats = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -60,8 +62,26 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkSchemaHealth = async () => {
|
||||||
|
setCheckingSchema(true)
|
||||||
|
try {
|
||||||
|
const result = await validateDatabaseSchema()
|
||||||
|
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
|
||||||
|
|
||||||
|
if (!result.valid) {
|
||||||
|
console.warn('Schema validation failed:', result.issues)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Schema check failed:', error)
|
||||||
|
setSchemaHealth('corrupted')
|
||||||
|
} finally {
|
||||||
|
setCheckingSchema(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStats()
|
loadStats()
|
||||||
|
checkSchemaHealth()
|
||||||
const config = loadStorageConfig()
|
const config = loadStorageConfig()
|
||||||
|
|
||||||
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||||
@@ -116,8 +136,9 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await clearDatabase()
|
await clearDatabase()
|
||||||
toast.success('Database cleared successfully')
|
toast.success('Database cleared and schema recreated successfully')
|
||||||
await loadStats()
|
await loadStats()
|
||||||
|
await checkSchemaHealth()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear:', error)
|
console.error('Failed to clear:', error)
|
||||||
toast.error('Failed to clear database')
|
toast.error('Failed to clear database')
|
||||||
@@ -247,6 +268,51 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 max-w-3xl">
|
<div className="grid gap-6 max-w-3xl">
|
||||||
|
{schemaHealth === 'corrupted' && (
|
||||||
|
<Card className="border-destructive bg-destructive/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<Warning weight="fill" size={24} />
|
||||||
|
Schema Corruption Detected
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Your database schema is outdated or corrupted and needs to be repaired
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Alert className="border-destructive">
|
||||||
|
<AlertDescription>
|
||||||
|
The database schema is missing required tables or columns (likely due to namespace feature addition).
|
||||||
|
This can cause errors when loading or saving snippets. Click the button below to wipe and recreate the database with the correct schema.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleClear} variant="destructive" className="gap-2">
|
||||||
|
<FirstAid weight="bold" size={16} />
|
||||||
|
Repair Database (Wipe & Recreate)
|
||||||
|
</Button>
|
||||||
|
<Button onClick={checkSchemaHealth} variant="outline" disabled={checkingSchema}>
|
||||||
|
{checkingSchema ? 'Checking...' : 'Re-check Schema'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{schemaHealth === 'healthy' && (
|
||||||
|
<Card className="border-green-600 bg-green-600/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-green-600">
|
||||||
|
<CheckCircle weight="fill" size={24} />
|
||||||
|
Schema Healthy
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Your database schema is up to date and functioning correctly
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{envVarSet && (
|
{envVarSet && (
|
||||||
<Card className="border-accent">
|
<Card className="border-accent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user