diff --git a/src/database/db.ts b/src/database/db.ts new file mode 100644 index 0000000..1056020 --- /dev/null +++ b/src/database/db.ts @@ -0,0 +1,69 @@ +import * as SQLite from 'expo-sqlite' +import { Measurement, SyncStatus } from '../types' + +let db: SQLite.SQLiteDatabase + +export async function initDatabase(): Promise { + db = await SQLite.openDatabaseAsync('weegschaal.db') + await db.execAsync(` + CREATE TABLE IF NOT EXISTS measurements ( + id TEXT PRIMARY KEY, + weight REAL NOT NULL, + bmi REAL NOT NULL, + bodyFatPercentage REAL NOT NULL, + fatMass REAL NOT NULL, + musclePercentage REAL NOT NULL, + muscleMass REAL NOT NULL, + visceralFat INTEGER NOT NULL, + bmr REAL NOT NULL, + measuredAt TEXT NOT NULL, + createdAt TEXT NOT NULL, + updatedAt TEXT NOT NULL, + syncStatus TEXT NOT NULL DEFAULT 'local' + ); + `) +} + +export async function insertMeasurement(m: Measurement): Promise { + await db.runAsync( + `INSERT INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, syncStatus) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.syncStatus] + ) +} + +export async function getAllMeasurements(): Promise { + const rows = await db.getAllAsync( + 'SELECT * FROM measurements ORDER BY measuredAt DESC' + ) + return rows as Measurement[] +} + +export async function getLatestMeasurement(): Promise { + const rows = await db.getAllAsync( + 'SELECT * FROM measurements ORDER BY measuredAt DESC LIMIT 1' + ) + return rows.length > 0 ? (rows[0] as Measurement) : null +} + +export async function updateMeasurement(id: string, syncStatus: SyncStatus): Promise { + await db.runAsync( + 'UPDATE measurements SET syncStatus = ?, updatedAt = ? WHERE id = ?', + [syncStatus, new Date().toISOString(), id] + ) +} + +export async function upsertMeasurement(m: Measurement): Promise { + await db.runAsync( + `INSERT OR REPLACE INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, syncStatus) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.syncStatus] + ) +} + +export async function getUnsyncedMeasurements(): Promise { + const rows = await db.getAllAsync( + "SELECT * FROM measurements WHERE syncStatus = 'local' ORDER BY updatedAt ASC" + ) + return rows as Measurement[] +} diff --git a/tests/database.test.ts b/tests/database.test.ts new file mode 100644 index 0000000..0a78538 --- /dev/null +++ b/tests/database.test.ts @@ -0,0 +1,79 @@ +import { initDatabase, insertMeasurement, getAllMeasurements, getLatestMeasurement, updateMeasurement } from '../src/database/db' +import { Measurement } from '../src/types' +import * as SQLite from 'expo-sqlite' + +jest.mock('expo-sqlite', () => ({ + openDatabaseAsync: jest.fn(), +})) + +const mockMeasurement: Measurement = { + id: '550e8400-e29b-41d4-a716-446655440000', + weight: 75.5, + bmi: 22.3, + bodyFatPercentage: 15.2, + fatMass: 11.5, + musclePercentage: 40.1, + muscleMass: 30.3, + visceralFat: 8, + bmr: 1650, + measuredAt: '2024-01-01T08:00:00.000Z', + createdAt: '2024-01-01T08:00:00.000Z', + updatedAt: '2024-01-01T08:00:00.000Z', + syncStatus: 'local', +} + +describe('database', () => { + let db: any + + beforeEach(() => { + db = { + execAsync: jest.fn(), + runAsync: jest.fn(), + getAllAsync: jest.fn(), + } + ;(SQLite.openDatabaseAsync as jest.Mock).mockResolvedValue(db) + }) + + it('initDatabase creates the measurements table', async () => { + await initDatabase() + expect(db.execAsync).toHaveBeenCalledWith( + expect.stringContaining('CREATE TABLE IF NOT EXISTS measurements') + ) + }) + + it('insertMeasurement inserts a row', async () => { + db.runAsync.mockResolvedValue(undefined) + await initDatabase() + await insertMeasurement(mockMeasurement) + expect(db.runAsync).toHaveBeenCalledWith( + expect.stringContaining('INSERT INTO measurements'), + expect.arrayContaining([mockMeasurement.id]) + ) + }) + + it('getAllMeasurements returns all measurements ordered by measuredAt desc', async () => { + db.getAllAsync.mockResolvedValue([{ ...mockMeasurement }]) + await initDatabase() + const result = await getAllMeasurements() + expect(result).toHaveLength(1) + expect(result[0].id).toBe(mockMeasurement.id) + }) + + it('getLatestMeasurement returns the most recent', async () => { + db.getAllAsync.mockResolvedValue([{ ...mockMeasurement }]) + await initDatabase() + const result = await getLatestMeasurement() + expect(result).not.toBeNull() + expect(result!.id).toBe(mockMeasurement.id) + }) + + it('updateMeasurement updates syncStatus', async () => { + db.runAsync.mockResolvedValue(undefined) + await initDatabase() + await updateMeasurement(mockMeasurement.id, 'synced') + expect(db.runAsync).toHaveBeenCalledWith( + expect.stringContaining('UPDATE measurements'), + expect.arrayContaining(['synced', mockMeasurement.id]) + ) + }) +})