feat: add database layer with SQLite CRUD
This commit is contained in:
parent
6c0b33f86a
commit
3842e8af63
2 changed files with 148 additions and 0 deletions
69
src/database/db.ts
Normal file
69
src/database/db.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import * as SQLite from 'expo-sqlite'
|
||||||
|
import { Measurement, SyncStatus } from '../types'
|
||||||
|
|
||||||
|
let db: SQLite.SQLiteDatabase
|
||||||
|
|
||||||
|
export async function initDatabase(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<Measurement[]> {
|
||||||
|
const rows = await db.getAllAsync<any>(
|
||||||
|
'SELECT * FROM measurements ORDER BY measuredAt DESC'
|
||||||
|
)
|
||||||
|
return rows as Measurement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestMeasurement(): Promise<Measurement | null> {
|
||||||
|
const rows = await db.getAllAsync<any>(
|
||||||
|
'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<void> {
|
||||||
|
await db.runAsync(
|
||||||
|
'UPDATE measurements SET syncStatus = ?, updatedAt = ? WHERE id = ?',
|
||||||
|
[syncStatus, new Date().toISOString(), id]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertMeasurement(m: Measurement): Promise<void> {
|
||||||
|
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<Measurement[]> {
|
||||||
|
const rows = await db.getAllAsync<any>(
|
||||||
|
"SELECT * FROM measurements WHERE syncStatus = 'local' ORDER BY updatedAt ASC"
|
||||||
|
)
|
||||||
|
return rows as Measurement[]
|
||||||
|
}
|
||||||
79
tests/database.test.ts
Normal file
79
tests/database.test.ts
Normal file
|
|
@ -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])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue