mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-23 16:05:31 +00:00
sqlite db implementation with development seeding and mock exercise library and more
This commit is contained in:
parent
76433b93e6
commit
7fd62bce37
24
CHANGELOG.md
24
CHANGELOG.md
@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- SQLite database implementation with development seeding
|
||||||
|
- Successfully integrated SQLite with proper transaction handling
|
||||||
|
- Added mock exercise library with 10 initial exercises
|
||||||
|
- Implemented development database seeder
|
||||||
|
- Added debug logging for database operations
|
||||||
|
- Event caching system for future Nostr integration
|
||||||
|
- Added EventCache service for Nostr event handling
|
||||||
|
- Implemented proper transaction management
|
||||||
|
- Added cache metadata tracking
|
||||||
|
- Database schema improvements
|
||||||
|
- Added nostr_events and event_tags tables
|
||||||
|
- Added cache_metadata table for performance optimization
|
||||||
|
- Added exercise_media table for future media support
|
||||||
- Alphabetical quick scroll in exercise library
|
- Alphabetical quick scroll in exercise library
|
||||||
- Dynamic letter highlighting for available sections
|
- Dynamic letter highlighting for available sections
|
||||||
- Smooth scrolling to selected sections
|
- Smooth scrolling to selected sections
|
||||||
@ -40,7 +53,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Exercise deletion functionality
|
- Exercise deletion functionality
|
||||||
- Keyboard overlap issues in exercise creation form
|
- Keyboard overlap issues in exercise creation form
|
||||||
- SQLite transaction handling for exercise operations
|
- SQLite transaction nesting issues
|
||||||
|
- TypeScript parameter typing in database services
|
||||||
|
- Null value handling in database operations
|
||||||
|
- Development seeding duplicate prevention
|
||||||
|
|
||||||
### Technical Details
|
### Technical Details
|
||||||
1. Database Schema Enforcement:
|
1. Database Schema Enforcement:
|
||||||
@ -61,6 +77,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Improved error propagation in LibraryService
|
- Improved error propagation in LibraryService
|
||||||
- Added transaction rollback on constraint violations
|
- Added transaction rollback on constraint violations
|
||||||
|
|
||||||
|
4. Database Services:
|
||||||
|
- Added EventCache service for Nostr events
|
||||||
|
- Improved ExerciseService with transaction awareness
|
||||||
|
- Added DevSeederService for development data
|
||||||
|
- Enhanced error handling and logging
|
||||||
|
|
||||||
### Migration Notes
|
### Migration Notes
|
||||||
- Exercise creation now enforces schema constraints
|
- Exercise creation now enforces schema constraints
|
||||||
- Input validation prevents invalid data entry
|
- Input validation prevents invalid data entry
|
||||||
|
@ -1,7 +1,25 @@
|
|||||||
|
// components/DatabaseProvider.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, ActivityIndicator, Text } from 'react-native';
|
import { View, ActivityIndicator, Text } from 'react-native';
|
||||||
import { SQLiteProvider, openDatabaseSync } from 'expo-sqlite';
|
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { schema } from '@/lib/db/schema';
|
import { schema } from '@/lib/db/schema';
|
||||||
|
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||||
|
import { EventCache } from '@/lib/db/services/EventCache';
|
||||||
|
import { DevSeederService } from '@/lib/db/services/DevSeederService';
|
||||||
|
import { logDatabaseInfo } from '@/lib/db/debug';
|
||||||
|
|
||||||
|
// Create context for services
|
||||||
|
interface DatabaseServicesContextValue {
|
||||||
|
exerciseService: ExerciseService | null;
|
||||||
|
eventCache: EventCache | null;
|
||||||
|
devSeeder: DevSeederService | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatabaseServicesContext = React.createContext<DatabaseServicesContextValue>({
|
||||||
|
exerciseService: null,
|
||||||
|
eventCache: null,
|
||||||
|
devSeeder: null,
|
||||||
|
});
|
||||||
|
|
||||||
interface DatabaseProviderProps {
|
interface DatabaseProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -10,6 +28,11 @@ interface DatabaseProviderProps {
|
|||||||
export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
||||||
const [isReady, setIsReady] = React.useState(false);
|
const [isReady, setIsReady] = React.useState(false);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
const [services, setServices] = React.useState<DatabaseServicesContextValue>({
|
||||||
|
exerciseService: null,
|
||||||
|
eventCache: null,
|
||||||
|
devSeeder: null,
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function initDatabase() {
|
async function initDatabase() {
|
||||||
@ -20,6 +43,26 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
console.log('[DB] Creating schema...');
|
console.log('[DB] Creating schema...');
|
||||||
await schema.createTables(db);
|
await schema.createTables(db);
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
console.log('[DB] Initializing services...');
|
||||||
|
const eventCache = new EventCache(db);
|
||||||
|
const exerciseService = new ExerciseService(db);
|
||||||
|
const devSeeder = new DevSeederService(db, exerciseService, eventCache);
|
||||||
|
|
||||||
|
// Set services
|
||||||
|
setServices({
|
||||||
|
exerciseService,
|
||||||
|
eventCache,
|
||||||
|
devSeeder,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed development database
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log('[DB] Seeding development database...');
|
||||||
|
await devSeeder.seedDatabase();
|
||||||
|
await logDatabaseInfo();
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DB] Database initialized successfully');
|
console.log('[DB] Database initialized successfully');
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -51,7 +94,34 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SQLiteProvider databaseName="powr.db">
|
<SQLiteProvider databaseName="powr.db">
|
||||||
|
<DatabaseServicesContext.Provider value={services}>
|
||||||
{children}
|
{children}
|
||||||
|
</DatabaseServicesContext.Provider>
|
||||||
</SQLiteProvider>
|
</SQLiteProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hooks for accessing services
|
||||||
|
export function useExerciseService() {
|
||||||
|
const context = React.useContext(DatabaseServicesContext);
|
||||||
|
if (!context.exerciseService) {
|
||||||
|
throw new Error('Exercise service not initialized');
|
||||||
|
}
|
||||||
|
return context.exerciseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEventCache() {
|
||||||
|
const context = React.useContext(DatabaseServicesContext);
|
||||||
|
if (!context.eventCache) {
|
||||||
|
throw new Error('Event cache not initialized');
|
||||||
|
}
|
||||||
|
return context.eventCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDevSeeder() {
|
||||||
|
const context = React.useContext(DatabaseServicesContext);
|
||||||
|
if (!context.devSeeder) {
|
||||||
|
throw new Error('Dev seeder not initialized');
|
||||||
|
}
|
||||||
|
return context.devSeeder;
|
||||||
|
}
|
@ -2,12 +2,11 @@
|
|||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 2;
|
export const SCHEMA_VERSION = 3; // Incrementing version for new tables
|
||||||
|
|
||||||
class Schema {
|
class Schema {
|
||||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||||
try {
|
try {
|
||||||
// First check if the table exists
|
|
||||||
const tableExists = await db.getFirstAsync<{ count: number }>(
|
const tableExists = await db.getFirstAsync<{ count: number }>(
|
||||||
`SELECT count(*) as count FROM sqlite_master
|
`SELECT count(*) as count FROM sqlite_master
|
||||||
WHERE type='table' AND name='schema_version'`
|
WHERE type='table' AND name='schema_version'`
|
||||||
@ -26,7 +25,7 @@ class Schema {
|
|||||||
return version?.version ?? 0;
|
return version?.version ?? 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[Schema] Error getting version:', error);
|
console.log('[Schema] Error getting version:', error);
|
||||||
return 0; // If table doesn't exist yet
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ class Schema {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (currentVersion === 0) {
|
if (currentVersion < 1) {
|
||||||
console.log('[Schema] Performing fresh install');
|
console.log('[Schema] Performing fresh install');
|
||||||
|
|
||||||
// Drop existing tables if they exist
|
// Drop existing tables if they exist
|
||||||
@ -79,7 +78,6 @@ class Schema {
|
|||||||
CREATE INDEX idx_exercise_tags ON exercise_tags(tag);
|
CREATE INDEX idx_exercise_tags ON exercise_tags(tag);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Set initial version
|
|
||||||
await db.runAsync(
|
await db.runAsync(
|
||||||
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||||
[1, Date.now()]
|
[1, Date.now()]
|
||||||
@ -88,7 +86,7 @@ class Schema {
|
|||||||
console.log('[Schema] Base tables created successfully');
|
console.log('[Schema] Base tables created successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update to version 2 if needed
|
// Update to version 2 if needed - Nostr support
|
||||||
if (currentVersion < 2) {
|
if (currentVersion < 2) {
|
||||||
console.log('[Schema] Upgrading to version 2');
|
console.log('[Schema] Upgrading to version 2');
|
||||||
|
|
||||||
@ -116,7 +114,7 @@ class Schema {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_event_tags ON event_tags(name, value);
|
CREATE INDEX IF NOT EXISTS idx_event_tags ON event_tags(name, value);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Add Nostr reference to exercises if not exists
|
// Add Nostr reference to exercises
|
||||||
try {
|
try {
|
||||||
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
|
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -131,6 +129,41 @@ class Schema {
|
|||||||
console.log('[Schema] Version 2 upgrade completed');
|
console.log('[Schema] Version 2 upgrade completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update to version 3 if needed - Event Cache
|
||||||
|
if (currentVersion < 3) {
|
||||||
|
console.log('[Schema] Upgrading to version 3');
|
||||||
|
|
||||||
|
// Create cache metadata table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS cache_metadata (
|
||||||
|
content_id TEXT PRIMARY KEY,
|
||||||
|
content_type TEXT NOT NULL,
|
||||||
|
last_accessed INTEGER NOT NULL,
|
||||||
|
access_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
cache_priority INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create media cache table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS exercise_media (
|
||||||
|
exercise_id TEXT NOT NULL,
|
||||||
|
media_type TEXT NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
thumbnail BLOB,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.runAsync(
|
||||||
|
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||||
|
[3, Date.now()]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[Schema] Version 3 upgrade completed');
|
||||||
|
}
|
||||||
|
|
||||||
// Verify final schema
|
// Verify final schema
|
||||||
const tables = await db.getAllAsync<{ name: string }>(
|
const tables = await db.getAllAsync<{ name: string }>(
|
||||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||||
|
97
lib/db/services/DevSeederService.ts
Normal file
97
lib/db/services/DevSeederService.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// lib/db/services/DevSeederService.ts
|
||||||
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
|
import { ExerciseService } from './ExerciseService';
|
||||||
|
import { EventCache } from './EventCache';
|
||||||
|
import { logDatabaseInfo } from '../debug';
|
||||||
|
import { mockExerciseEvents, convertNostrToExercise } from '../../mocks/exercises';
|
||||||
|
|
||||||
|
export class DevSeederService {
|
||||||
|
private db: SQLiteDatabase;
|
||||||
|
private exerciseService: ExerciseService;
|
||||||
|
private eventCache: EventCache;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
db: SQLiteDatabase,
|
||||||
|
exerciseService: ExerciseService,
|
||||||
|
eventCache: EventCache
|
||||||
|
) {
|
||||||
|
this.db = db;
|
||||||
|
this.exerciseService = exerciseService;
|
||||||
|
this.eventCache = eventCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async seedDatabase() {
|
||||||
|
if (!__DEV__) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting development database seeding...');
|
||||||
|
|
||||||
|
// Log initial database state
|
||||||
|
await logDatabaseInfo();
|
||||||
|
|
||||||
|
// Check if we already have exercises
|
||||||
|
const existingCount = (await this.exerciseService.getAllExercises()).length;
|
||||||
|
|
||||||
|
if (existingCount > 0) {
|
||||||
|
console.log('Database already seeded with', existingCount, 'exercises');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction for all seeding operations
|
||||||
|
await this.db.withTransactionAsync(async () => {
|
||||||
|
console.log('Seeding mock exercises...');
|
||||||
|
|
||||||
|
// Process all events within the same transaction
|
||||||
|
for (const event of mockExerciseEvents) {
|
||||||
|
// Pass true to indicate we're in a transaction
|
||||||
|
await this.eventCache.setEvent(event, true);
|
||||||
|
const exercise = convertNostrToExercise(event);
|
||||||
|
await this.exerciseService.createExercise(exercise, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully seeded', mockExerciseEvents.length, 'exercises');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log final database state
|
||||||
|
await logDatabaseInfo();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding database:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearDatabase() {
|
||||||
|
if (!__DEV__) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Clearing development database...');
|
||||||
|
|
||||||
|
await this.db.withTransactionAsync(async () => {
|
||||||
|
const tables = [
|
||||||
|
'exercises',
|
||||||
|
'exercise_tags',
|
||||||
|
'nostr_events',
|
||||||
|
'event_tags',
|
||||||
|
'cache_metadata'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
await this.db.runAsync(`DELETE FROM ${table}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Successfully cleared database');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing database:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetDatabase() {
|
||||||
|
if (!__DEV__) return;
|
||||||
|
|
||||||
|
await this.clearDatabase();
|
||||||
|
await this.seedDatabase();
|
||||||
|
}
|
||||||
|
}
|
115
lib/db/services/EventCache.ts
Normal file
115
lib/db/services/EventCache.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// lib/db/services/EventCache.ts
|
||||||
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
|
import { NostrEvent } from '@/types/nostr';
|
||||||
|
|
||||||
|
export class EventCache {
|
||||||
|
private db: SQLiteDatabase;
|
||||||
|
private writeBuffer: { query: string; params: any[] }[] = [];
|
||||||
|
|
||||||
|
constructor(db: SQLiteDatabase) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEvent(event: NostrEvent, inTransaction: boolean = false): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Store queries to execute
|
||||||
|
const queries = [
|
||||||
|
{
|
||||||
|
query: `INSERT OR REPLACE INTO nostr_events
|
||||||
|
(id, pubkey, kind, created_at, content, sig, raw_event, received_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
params: [
|
||||||
|
event.id || '', // Convert undefined to empty string
|
||||||
|
event.pubkey || '',
|
||||||
|
event.kind,
|
||||||
|
event.created_at,
|
||||||
|
event.content,
|
||||||
|
event.sig || '',
|
||||||
|
JSON.stringify(event),
|
||||||
|
Date.now()
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Add metadata query
|
||||||
|
{
|
||||||
|
query: `INSERT OR REPLACE INTO cache_metadata
|
||||||
|
(content_id, content_type, last_accessed, access_count)
|
||||||
|
VALUES (?, ?, ?, 1)
|
||||||
|
ON CONFLICT(content_id) DO UPDATE SET
|
||||||
|
last_accessed = ?,
|
||||||
|
access_count = access_count + 1`,
|
||||||
|
params: [
|
||||||
|
event.id || '',
|
||||||
|
'event',
|
||||||
|
Date.now(),
|
||||||
|
Date.now()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add tag queries
|
||||||
|
event.tags.forEach((tag, index) => {
|
||||||
|
queries.push({
|
||||||
|
query: `INSERT OR REPLACE INTO event_tags
|
||||||
|
(event_id, name, value, index_num)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
params: [
|
||||||
|
event.id || '',
|
||||||
|
tag[0] || '',
|
||||||
|
tag[1] || '',
|
||||||
|
index
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're already in a transaction, just execute the queries
|
||||||
|
if (inTransaction) {
|
||||||
|
for (const { query, params } of queries) {
|
||||||
|
await this.db.runAsync(query, params);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, wrap in our own transaction
|
||||||
|
await this.db.withTransactionAsync(async () => {
|
||||||
|
for (const { query, params } of queries) {
|
||||||
|
await this.db.runAsync(query, params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error caching event:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(id: string): Promise<NostrEvent | null> {
|
||||||
|
try {
|
||||||
|
const event = await this.db.getFirstAsync<any>(
|
||||||
|
`SELECT * FROM nostr_events WHERE id = ?`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!event) return null;
|
||||||
|
|
||||||
|
// Get tags
|
||||||
|
const tags = await this.db.getAllAsync<{ name: string; value: string }>(
|
||||||
|
`SELECT name, value FROM event_tags WHERE event_id = ? ORDER BY index_num`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update access metadata
|
||||||
|
await this.db.runAsync(
|
||||||
|
`UPDATE cache_metadata
|
||||||
|
SET last_accessed = ?, access_count = access_count + 1
|
||||||
|
WHERE content_id = ?`,
|
||||||
|
[Date.now(), id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
tags: tags.map(tag => [tag.name, tag.value])
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting event from cache:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,12 +43,15 @@ export class ExerciseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update createExercise to handle all required fields
|
// Update createExercise to handle all required fields
|
||||||
async createExercise(exercise: Omit<Exercise, 'id' | 'availability'>): Promise<string> {
|
async createExercise(
|
||||||
|
exercise: Omit<Exercise, 'id' | 'availability'>,
|
||||||
|
inTransaction: boolean = false
|
||||||
|
): Promise<string> {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.db.withTransactionAsync(async () => {
|
const runQueries = async () => {
|
||||||
await this.db.runAsync(
|
await this.db.runAsync(
|
||||||
`INSERT INTO exercises (
|
`INSERT INTO exercises (
|
||||||
id, title, type, category, equipment, description,
|
id, title, type, category, equipment, description,
|
||||||
@ -77,7 +80,13 @@ export class ExerciseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (inTransaction) {
|
||||||
|
await runQueries();
|
||||||
|
} else {
|
||||||
|
await this.db.withTransactionAsync(runQueries);
|
||||||
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
277
lib/mocks/exercises.ts
Normal file
277
lib/mocks/exercises.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
// lib/mocks/exercises.ts
|
||||||
|
import { NostrEvent } from '@/types/nostr';
|
||||||
|
import { Exercise, ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||||
|
import { generateId } from '@/utils/ids';
|
||||||
|
|
||||||
|
// Mock exercise definitions that will become our initial POWR library
|
||||||
|
export const mockExerciseEvents: NostrEvent[] = [
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Stand with feet hip-width apart, barbell racked on shoulders. Bend knees and hips to squat down, keeping chest up. Drive through heels to stand.",
|
||||||
|
tags: [
|
||||||
|
["d", "bb-back-squat"],
|
||||||
|
["title", "Barbell Back Squat"],
|
||||||
|
["format", "weight", "reps", "rpe", "set_type"],
|
||||||
|
["format_units", "kg", "count", "0-10", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "barbell"],
|
||||||
|
["difficulty", "intermediate"],
|
||||||
|
["category", "legs"],
|
||||||
|
["t", "compound"],
|
||||||
|
["t", "squat"],
|
||||||
|
["t", "legs"],
|
||||||
|
["t", "quadriceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800, // Feb 19, 2024
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr", // We'll update this when we create the POWR relay
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Stand with feet shoulder-width apart, barbell on floor. Hinge at hips, grip bar outside knees. Keep back flat, drive through heels to lift.",
|
||||||
|
tags: [
|
||||||
|
["d", "bb-deadlift"],
|
||||||
|
["title", "Barbell Deadlift"],
|
||||||
|
["format", "weight", "reps", "rpe", "set_type"],
|
||||||
|
["format_units", "kg", "count", "0-10", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "barbell"],
|
||||||
|
["difficulty", "intermediate"],
|
||||||
|
["category", "legs"],
|
||||||
|
["t", "compound"],
|
||||||
|
["t", "hinge"],
|
||||||
|
["t", "legs"],
|
||||||
|
["t", "posterior"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Lie on bench, feet flat on floor. Grip barbell slightly wider than shoulders. Lower bar to chest, press back up to start.",
|
||||||
|
tags: [
|
||||||
|
["d", "bb-bench-press"],
|
||||||
|
["title", "Barbell Bench Press"],
|
||||||
|
["format", "weight", "reps", "rpe", "set_type"],
|
||||||
|
["format_units", "kg", "count", "0-10", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "barbell"],
|
||||||
|
["difficulty", "intermediate"],
|
||||||
|
["category", "push"],
|
||||||
|
["t", "compound"],
|
||||||
|
["t", "push"],
|
||||||
|
["t", "chest"],
|
||||||
|
["t", "triceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Start in plank position. Lower body by bending elbows, keeping body straight. Push back up to start position.",
|
||||||
|
tags: [
|
||||||
|
["d", "pushup"],
|
||||||
|
["title", "Push-Up"],
|
||||||
|
["format", "reps", "set_type"],
|
||||||
|
["format_units", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "bodyweight"],
|
||||||
|
["difficulty", "beginner"],
|
||||||
|
["category", "push"],
|
||||||
|
["t", "bodyweight"],
|
||||||
|
["t", "push"],
|
||||||
|
["t", "chest"],
|
||||||
|
["t", "triceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Hang from pull-up bar with overhand grip. Pull body up until chin clears bar, lower back to start.",
|
||||||
|
tags: [
|
||||||
|
["d", "pullup"],
|
||||||
|
["title", "Pull-Up"],
|
||||||
|
["format", "reps", "set_type"],
|
||||||
|
["format_units", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "bodyweight"],
|
||||||
|
["difficulty", "intermediate"],
|
||||||
|
["category", "pull"],
|
||||||
|
["t", "bodyweight"],
|
||||||
|
["t", "pull"],
|
||||||
|
["t", "back"],
|
||||||
|
["t", "biceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Sit at machine, grip handles at shoulder height. Press handles up overhead, return to start position.",
|
||||||
|
tags: [
|
||||||
|
["d", "shoulder-press-machine"],
|
||||||
|
["title", "Shoulder Press Machine"],
|
||||||
|
["format", "weight", "reps", "set_type"],
|
||||||
|
["format_units", "kg", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "machine"],
|
||||||
|
["difficulty", "beginner"],
|
||||||
|
["category", "push"],
|
||||||
|
["t", "machine"],
|
||||||
|
["t", "push"],
|
||||||
|
["t", "shoulders"],
|
||||||
|
["t", "triceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Stand with dumbbell in each hand at sides. Curl weights toward shoulders, keeping elbows close to body. Lower back down.",
|
||||||
|
tags: [
|
||||||
|
["d", "db-bicep-curl"],
|
||||||
|
["title", "Dumbbell Bicep Curl"],
|
||||||
|
["format", "weight", "reps", "set_type"],
|
||||||
|
["format_units", "kg", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "dumbbell"],
|
||||||
|
["difficulty", "beginner"],
|
||||||
|
["category", "pull"],
|
||||||
|
["t", "isolation"],
|
||||||
|
["t", "pull"],
|
||||||
|
["t", "biceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Attach rope to cable machine at top. Grip ends, pull down to chest level keeping elbows close. Control return.",
|
||||||
|
tags: [
|
||||||
|
["d", "cable-tricep-pushdown"],
|
||||||
|
["title", "Cable Tricep Pushdown"],
|
||||||
|
["format", "weight", "reps", "set_type"],
|
||||||
|
["format_units", "kg", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "cable"],
|
||||||
|
["difficulty", "beginner"],
|
||||||
|
["category", "push"],
|
||||||
|
["t", "isolation"],
|
||||||
|
["t", "push"],
|
||||||
|
["t", "triceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Kneel before cable machine, rope attachment at bottom. Pull rope toward forehead, keeping upper arms still. Lower with control.",
|
||||||
|
tags: [
|
||||||
|
["d", "cable-face-pull"],
|
||||||
|
["title", "Cable Face Pull"],
|
||||||
|
["format", "weight", "reps", "set_type"],
|
||||||
|
["format_units", "kg", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "cable"],
|
||||||
|
["difficulty", "intermediate"],
|
||||||
|
["category", "pull"],
|
||||||
|
["t", "isolation"],
|
||||||
|
["t", "pull"],
|
||||||
|
["t", "rear-deltoids"],
|
||||||
|
["t", "upper-back"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 33401,
|
||||||
|
content: "Stand with feet hip-width, holding kettlebell by horns at chest. Squat down keeping chest up, stand back up.",
|
||||||
|
tags: [
|
||||||
|
["d", "kb-goblet-squat"],
|
||||||
|
["title", "Kettlebell Goblet Squat"],
|
||||||
|
["format", "weight", "reps", "set_type"],
|
||||||
|
["format_units", "kg", "count", "warmup|normal|drop|failure"],
|
||||||
|
["equipment", "kettlebell"],
|
||||||
|
["difficulty", "beginner"],
|
||||||
|
["category", "legs"],
|
||||||
|
["t", "compound"],
|
||||||
|
["t", "squat"],
|
||||||
|
["t", "legs"],
|
||||||
|
["t", "quadriceps"]
|
||||||
|
],
|
||||||
|
created_at: 1708300800,
|
||||||
|
id: generateId('nostr'),
|
||||||
|
pubkey: "powr",
|
||||||
|
sig: undefined
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function getTagValue(tags: string[][], name: string): string | undefined {
|
||||||
|
const tag = tags.find((tag: string[]) => tag[0] === name);
|
||||||
|
return tag ? tag[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTags(tags: string[][]): string[] {
|
||||||
|
return tags
|
||||||
|
.filter((tag: string[]) => tag[0] === 't')
|
||||||
|
.map((tag: string[]) => tag[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertNostrToExercise(event: NostrEvent): Exercise {
|
||||||
|
return {
|
||||||
|
id: event.id || '',
|
||||||
|
title: getTagValue(event.tags, 'title') || '',
|
||||||
|
type: getTagValue(event.tags, 'equipment') === 'bodyweight'
|
||||||
|
? 'bodyweight'
|
||||||
|
: 'strength' as ExerciseType,
|
||||||
|
category: getTagValue(event.tags, 'category') as ExerciseCategory,
|
||||||
|
equipment: getTagValue(event.tags, 'equipment') as Equipment,
|
||||||
|
description: event.content,
|
||||||
|
format: getTagValue(event.tags, 'format')
|
||||||
|
?.split(',')
|
||||||
|
.reduce((acc: Record<string, boolean>, curr: string) => ({
|
||||||
|
...acc,
|
||||||
|
[curr]: true
|
||||||
|
}), {}),
|
||||||
|
format_units: getTagValue(event.tags, 'format_units')
|
||||||
|
?.split(',')
|
||||||
|
.reduce((acc: Record<string, string>, curr: string, i: number) => {
|
||||||
|
const format = getTagValue(event.tags, 'format')?.split(',')[i];
|
||||||
|
return format ? { ...acc, [format]: curr } : acc;
|
||||||
|
}, {}),
|
||||||
|
tags: getTags(event.tags),
|
||||||
|
availability: {
|
||||||
|
source: ['powr']
|
||||||
|
},
|
||||||
|
created_at: event.created_at * 1000,
|
||||||
|
source: 'powr'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export pre-converted exercises for easy testing
|
||||||
|
export const mockExercises = mockExerciseEvents.map(convertNostrToExercise);
|
||||||
|
|
||||||
|
// Helper to seed the database
|
||||||
|
export async function seedExercises(exerciseService: any) {
|
||||||
|
try {
|
||||||
|
const existingCount = (await exerciseService.getAllExercises()).length;
|
||||||
|
if (existingCount === 0) {
|
||||||
|
console.log('Seeding database with mock exercises...');
|
||||||
|
for (const exercise of mockExercises) {
|
||||||
|
await exerciseService.createExercise(exercise);
|
||||||
|
}
|
||||||
|
console.log('Successfully seeded database');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error seeding database:', error);
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"]
|
||||||
"*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
32
types/nostr.ts
Normal file
32
types/nostr.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// types/nostr.ts
|
||||||
|
export interface NostrEvent {
|
||||||
|
id?: string;
|
||||||
|
pubkey?: string;
|
||||||
|
content: string;
|
||||||
|
created_at: number;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
sig?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NostrEventKind {
|
||||||
|
EXERCISE = 33401,
|
||||||
|
TEMPLATE = 33402,
|
||||||
|
WORKOUT = 33403
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NostrTag {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
export function getTagValue(tags: string[][], name: string): string | undefined {
|
||||||
|
const tag = tags.find(t => t[0] === name);
|
||||||
|
return tag ? tag[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagValues(tags: string[][], name: string): string[] {
|
||||||
|
return tags.filter(t => t[0] === name).map(t => t[1]);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user