diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6f7f7a6..905b9c9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -29,9 +29,18 @@ jobs: id: docker_tag run: echo "TAG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV + - name: Set up QEMU for cross-platform builds + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + - name: Build and Push Docker image for Testing uses: docker/build-push-action@v5 with: context: . push: true - tags: ${{ env.TAG }} \ No newline at end of file + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ env.TAG }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ad0dc7..cf51481 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,11 +27,20 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU for cross-platform builds + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + - name: Build and Push Docker image uses: docker/build-push-action@v5 with: context: . push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} diff --git a/package-lock.json b/package-lock.json index 116d5ef..9f80961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2629,6 +2629,17 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.5.0.tgz", + "integrity": "sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -10234,13 +10245,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "better-sqlite3": "^11.5.0", "body-parser": "^1.20.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", - "openai": "^4.67.3", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7" + "openai": "^4.67.3" }, "devDependencies": { "jest": "^29.7.0", diff --git a/server/package.json b/server/package.json index 6eb2777..1cb527d 100644 --- a/server/package.json +++ b/server/package.json @@ -11,13 +11,12 @@ "license": "ISC", "description": "", "dependencies": { + "better-sqlite3": "^11.5.0", "body-parser": "^1.20.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", - "openai": "^4.67.3", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7" + "openai": "^4.67.3" }, "devDependencies": { "jest": "^29.7.0", diff --git a/server/src/config/database.js b/server/src/config/database.js index 3cb519a..ffdd759 100644 --- a/server/src/config/database.js +++ b/server/src/config/database.js @@ -1,5 +1,4 @@ -const sqlite3 = require('sqlite3'); -const { open } = require('sqlite'); +const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); @@ -23,21 +22,23 @@ function getDatabasePath() { } } -async function initializeDatabase() { +function initializeDatabase() { try { const dbPath = getDatabasePath(); console.log(`Initializing SQLite database at: ${dbPath}`); - db = await open({ - filename: dbPath, - driver: sqlite3.Database + // Open database with WAL mode for better performance + db = new Database(dbPath, { + verbose: console.log, + fileMustExist: false }); - // Enable foreign keys - await db.run('PRAGMA foreign_keys = ON'); + // Enable foreign keys and WAL mode + db.pragma('foreign_keys = ON'); + db.pragma('journal_mode = WAL'); // Create snippets table with timestamp - await db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS snippets ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, @@ -61,12 +62,11 @@ async function createBackup() { const dbPath = getDatabasePath(); const backupPath = getBackupPath(); - // Check if source database exists if (!fs.existsSync(dbPath)) { throw new Error('Source database does not exist'); } - // Create backup using stream to handle large files + // Backup using stream await new Promise((resolve, reject) => { const readStream = fs.createReadStream(dbPath); const writeStream = fs.createWriteStream(backupPath); @@ -78,7 +78,6 @@ async function createBackup() { readStream.pipe(writeStream); }); - // Verify backup file exists and has content const backupStats = fs.statSync(backupPath); if (backupStats.size === 0) { throw new Error('Backup file was created but is empty'); diff --git a/server/src/repositories/snippetRepository.js b/server/src/repositories/snippetRepository.js index 227e91a..1e5ffed 100644 --- a/server/src/repositories/snippetRepository.js +++ b/server/src/repositories/snippetRepository.js @@ -1,54 +1,86 @@ const { getDb } = require('../config/database'); class SnippetRepository { - // Helper method to format SELECT statements with proper UTC handling - #getSelectQuery(additional = '') { - return ` - SELECT - id, - title, - language, - description, - code, - datetime(updated_at) || 'Z' as updated_at - FROM snippets - ${additional} - `.trim(); + constructor() { + this.selectAllStmt = null; + this.insertStmt = null; + this.updateStmt = null; + this.deleteStmt = null; + this.selectByIdStmt = null; } - async findAll() { + #initializeStatements() { const db = getDb(); + + if (!this.selectAllStmt) { + this.selectAllStmt = db.prepare(` + SELECT + id, + title, + language, + description, + code, + datetime(updated_at) || 'Z' as updated_at + FROM snippets + ORDER BY updated_at DESC + `); + + this.insertStmt = db.prepare(` + INSERT INTO snippets ( + title, + language, + description, + code, + updated_at + ) VALUES (?, ?, ?, ?, datetime('now', 'utc')) + `); + + this.updateStmt = db.prepare(` + UPDATE snippets + SET title = ?, + language = ?, + description = ?, + code = ?, + updated_at = datetime('now', 'utc') + WHERE id = ? + `); + + this.deleteStmt = db.prepare('DELETE FROM snippets WHERE id = ?'); + + this.selectByIdStmt = db.prepare(` + SELECT + id, + title, + language, + description, + code, + datetime(updated_at) || 'Z' as updated_at + FROM snippets + WHERE id = ? + `); + } + } + + findAll() { + this.#initializeStatements(); try { - const snippets = await db.all( - this.#getSelectQuery('ORDER BY updated_at DESC') - ); - return snippets; + return this.selectAllStmt.all(); } catch (error) { console.error('Error in findAll:', error); throw error; } } - async create({ title, language, description, code }) { - const db = getDb(); + create({ title, language, description, code }) { + this.#initializeStatements(); try { - const result = await db.run( - `INSERT INTO snippets ( - title, - language, - description, - code, - updated_at - ) VALUES (?, ?, ?, ?, datetime('now', 'utc'))`, - [title, language, description, code] - ); + const db = getDb(); + const result = db.transaction(() => { + const insertResult = this.insertStmt.run(title, language, description, code); + return this.selectByIdStmt.get(insertResult.lastInsertRowid); + })(); - // Fetch the created snippet with UTC formatting - const created = await db.get( - this.#getSelectQuery('WHERE id = ?'), - [result.lastID] - ); - return created; + return result; } catch (error) { console.error('Error in create:', error); console.error('Parameters:', { title, language, description, code }); @@ -56,45 +88,31 @@ class SnippetRepository { } } - async delete(id) { - const db = getDb(); + delete(id) { + this.#initializeStatements(); try { - // Only fetch necessary fields for deletion confirmation - const snippet = await db.get( - this.#getSelectQuery('WHERE id = ?'), - [id] - ); - - if (snippet) { - await db.run('DELETE FROM snippets WHERE id = ?', [id]); - } - - return snippet; + const db = getDb(); + return db.transaction(() => { + const snippet = this.selectByIdStmt.get(id); + if (snippet) { + this.deleteStmt.run(id); + } + return snippet; + })(); } catch (error) { console.error('Error in delete:', error); throw error; } } - async update(id, { title, language, description, code }) { - const db = getDb(); + update(id, { title, language, description, code }) { + this.#initializeStatements(); try { - await db.run( - `UPDATE snippets - SET title = ?, - language = ?, - description = ?, - code = ?, - updated_at = datetime('now', 'utc') - WHERE id = ?`, - [title, language, description, code, id] - ); - - // Return updated snippet with UTC formatting - return db.get( - this.#getSelectQuery('WHERE id = ?'), - [id] - ); + const db = getDb(); + return db.transaction(() => { + this.updateStmt.run(title, language, description, code, id); + return this.selectByIdStmt.get(id); + })(); } catch (error) { console.error('Error in update:', error); throw error;