Dart Shelf API with modules: auth (JWT + PBKDF2), tasks (CRUD + dopamine scorer), streaks (forgiveness + freeze), rewards (variable reward engine), time perception, sync (offline-first push/pull), rooms (body doubling placeholder). Includes DB migration (001_initial_schema.sql) and Docker Compose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.1 KiB
Dart
161 lines
5.1 KiB
Dart
import '../../config/database.dart';
|
|
|
|
/// Data access layer for streaks and streak entries.
|
|
class StreakRepository {
|
|
// ── Create ──────────────────────────────────────────────────────────
|
|
|
|
Future<Map<String, dynamic>> create(Map<String, dynamic> data) async {
|
|
final result = await Database.query(
|
|
'''
|
|
INSERT INTO streaks (
|
|
id, user_id, name, description, frequency, grace_days,
|
|
current_count, longest_count, frozen_until,
|
|
created_at, updated_at
|
|
) VALUES (
|
|
@id, @user_id, @name, @description, @frequency, @grace_days,
|
|
0, 0, NULL,
|
|
NOW(), NOW()
|
|
)
|
|
RETURNING *
|
|
''',
|
|
parameters: data,
|
|
);
|
|
return _rowToMap(result.first);
|
|
}
|
|
|
|
// ── Read ────────────────────────────────────────────────────────────
|
|
|
|
Future<Map<String, dynamic>?> findById(String id, String userId) async {
|
|
final result = await Database.query(
|
|
'''
|
|
SELECT * FROM streaks
|
|
WHERE id = @id AND user_id = @user_id
|
|
''',
|
|
parameters: {'id': id, 'user_id': userId},
|
|
);
|
|
if (result.isEmpty) return null;
|
|
return _rowToMap(result.first);
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> findAll(String userId) async {
|
|
final result = await Database.query(
|
|
'''
|
|
SELECT * FROM streaks
|
|
WHERE user_id = @user_id
|
|
ORDER BY created_at DESC
|
|
''',
|
|
parameters: {'user_id': userId},
|
|
);
|
|
return result.map(_rowToMap).toList();
|
|
}
|
|
|
|
// ── Update ──────────────────────────────────────────────────────────
|
|
|
|
Future<Map<String, dynamic>?> update(
|
|
String id,
|
|
String userId,
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
final setClauses = <String>[];
|
|
final params = <String, dynamic>{'id': id, 'user_id': userId};
|
|
|
|
data.forEach((key, value) {
|
|
setClauses.add('$key = @$key');
|
|
params[key] = value;
|
|
});
|
|
setClauses.add('updated_at = NOW()');
|
|
|
|
final result = await Database.query(
|
|
'''
|
|
UPDATE streaks SET ${setClauses.join(', ')}
|
|
WHERE id = @id AND user_id = @user_id
|
|
RETURNING *
|
|
''',
|
|
parameters: params,
|
|
);
|
|
if (result.isEmpty) return null;
|
|
return _rowToMap(result.first);
|
|
}
|
|
|
|
// ── Entries ─────────────────────────────────────────────────────────
|
|
|
|
Future<void> addEntry(Map<String, dynamic> data) async {
|
|
await Database.query(
|
|
'''
|
|
INSERT INTO streak_entries (id, streak_id, entry_date, entry_type, note, created_at)
|
|
VALUES (@id, @streak_id, @entry_date, @entry_type, @note, NOW())
|
|
ON CONFLICT (streak_id, entry_date) DO NOTHING
|
|
''',
|
|
parameters: data,
|
|
);
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> getHistory(
|
|
String streakId,
|
|
String userId,
|
|
) async {
|
|
final result = await Database.query(
|
|
'''
|
|
SELECT se.* FROM streak_entries se
|
|
JOIN streaks s ON se.streak_id = s.id
|
|
WHERE se.streak_id = @streak_id AND s.user_id = @user_id
|
|
ORDER BY se.entry_date DESC
|
|
LIMIT 90
|
|
''',
|
|
parameters: {'streak_id': streakId, 'user_id': userId},
|
|
);
|
|
return result
|
|
.map((row) => <String, dynamic>{
|
|
'id': row[0],
|
|
'streak_id': row[1],
|
|
'entry_date': row[2] is DateTime
|
|
? (row[2] as DateTime).toIso8601String()
|
|
: row[2],
|
|
'entry_type': row[3],
|
|
'note': row[4],
|
|
'created_at': row[5] is DateTime
|
|
? (row[5] as DateTime).toIso8601String()
|
|
: row[5],
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
Future<DateTime?> lastCompletionDate(String streakId) async {
|
|
final result = await Database.query(
|
|
'''
|
|
SELECT MAX(entry_date) FROM streak_entries
|
|
WHERE streak_id = @streak_id AND entry_type = 'completion'
|
|
''',
|
|
parameters: {'streak_id': streakId},
|
|
);
|
|
if (result.isEmpty || result.first[0] == null) return null;
|
|
final val = result.first[0];
|
|
if (val is DateTime) return val;
|
|
return DateTime.tryParse(val.toString());
|
|
}
|
|
|
|
// ── Row mapper ──────────────────────────────────────────────────────
|
|
|
|
Map<String, dynamic> _rowToMap(dynamic row) {
|
|
final columns = [
|
|
'id',
|
|
'user_id',
|
|
'name',
|
|
'description',
|
|
'frequency',
|
|
'grace_days',
|
|
'current_count',
|
|
'longest_count',
|
|
'frozen_until',
|
|
'created_at',
|
|
'updated_at',
|
|
];
|
|
final map = <String, dynamic>{};
|
|
for (var i = 0; i < columns.length; i++) {
|
|
final value = row[i];
|
|
map[columns[i]] = value is DateTime ? value.toIso8601String() : value;
|
|
}
|
|
return map;
|
|
}
|
|
}
|