Neue Datenbank Struktur #156

Open
gelbeinhalb wants to merge 88 commits from feature/88-neue-datenbank-struktur into development
5 changed files with 176 additions and 50 deletions
Showing only changes of commit 7339194ba0 - Show all commits

View File

@@ -15,6 +15,43 @@
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"description": {
"type": ["string", "null"]
}
},
"required": [
"id",
"createdAt",
"name"
]
}
},
"games": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"ruleset": {
"type": ["string", "null"]
},
"description": {
"type": ["string", "null"]
},
"color": {
"type": ["integer", "null"]
},
"icon": {
"type": ["string", "null"]
} }
}, },
"required": [ "required": [
@@ -25,6 +62,38 @@
} }
}, },
"groups": { "groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": ["string", "null"]
},
"memberIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"name",
"createdAt",
"memberIds"
]
}
},
"teams": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
@@ -67,6 +136,12 @@
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
"gameId": {
"anyOf": [
{"type": "string"},
{"type": "null"}
]
},
"groupId": { "groupId": {
flixcoo marked this conversation as resolved
Review

Was soll endedAt für einen Sinn haben? Also wozu brauche ich den Endzeitpunkt eines Spiels? Und vor allem wie lege ich den Fest? Wenn ich denn Winner setze?

Was soll `endedAt` für einen Sinn haben? Also wozu brauche ich den Endzeitpunkt eines Spiels? Und vor allem wie lege ich den Fest? Wenn ich denn Winner setze?
Review

Ich dachte, dass man den Zeitpunkt später braucht, wenn das Winner Attribut entfernt wird. Später soll das ja nur calculated werden basierend auf den scores der Spieler und nicht extra gespeichert werden. Dann braucht man ja einen Weg zu sagen, ob das Spiel fertig ist oder nicht. Einen endedAt Timestamp fand ich besser als einen einfachen finished boolean

Ich dachte, dass man den Zeitpunkt später braucht, wenn das Winner Attribut entfernt wird. Später soll das ja nur calculated werden basierend auf den scores der Spieler und nicht extra gespeichert werden. Dann braucht man ja einen Weg zu sagen, ob das Spiel fertig ist oder nicht. Einen `endedAt` Timestamp fand ich besser als einen einfachen `finished` boolean
Review

ah okay, ja fair

ah okay, ja fair
"anyOf": [ "anyOf": [
{"type": "string"}, {"type": "string"},
@@ -79,7 +154,7 @@
"type": "string" "type": "string"
} }
}, },
"winnerId": { "notes": {
"anyOf": [ "anyOf": [
{"type": "string"}, {"type": "string"},
{"type": "null"} {"type": "null"}
@@ -90,7 +165,6 @@
"id", "id",
"name", "name",
"createdAt", "createdAt",
"groupId",
"playerIds" "playerIds"
] ]
} }
@@ -98,7 +172,9 @@
}, },
"required": [ "required": [
"players", "players",
"games",
"groups", "groups",
"teams",
"matches" "matches"
] ]
} }

View File

@@ -23,22 +23,21 @@ class Group {
return 'Group{id: $id, name: $name, description: $description, members: $members}'; return 'Group{id: $id, name: $name, description: $description, members: $members}';
} }
/// Creates a Group instance from a JSON object. /// Creates a Group instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
Group.fromJson(Map<String, dynamic> json) Group.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
description = json['description'], description = json['description'],
members = (json['members'] as List) members = []; // Populated during import via DataTransferService
.map((memberJson) => Player.fromJson(memberJson))
.toList();
/// Converts the Group instance to a JSON object. /// Converts the Group instance to a JSON object using normalized format (memberIds only).
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'name': name, 'name': name,
'description': description, 'description': description,
'members': members.map((member) => member.toJson()).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -28,31 +28,28 @@ class Match {
@override @override
String toString() { String toString() {
return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes, winner: $winner}'; return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes}';
} }
/// Creates a Match instance from a JSON object. /// Creates a Match instance from a JSON object (ID references format).
/// Related objects are reconstructed from IDs by the DataTransferService.
Match.fromJson(Map<String, dynamic> json) Match.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
name = json['name'], name = json['name'],
game = json['game'] != null ? Game.fromJson(json['game']) : null, game = null, // Populated during import via DataTransferService
group = json['group'] != null ? Group.fromJson(json['group']) : null, group = null, // Populated during import via DataTransferService
players = json['players'] != null players = [], // Populated during import via DataTransferService
? (json['players'] as List)
.map((playerJson) => Player.fromJson(playerJson))
.toList()
: null,
notes = json['notes']; notes = json['notes'];
/// Converts the Match instance to a JSON object. /// Converts the Match instance to a JSON object using normalized format (ID references only).
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'name': name, 'name': name,
'game': game?.toJson(), 'gameId': game?.id,
'group': group?.toJson(), 'groupId': group?.id,
'players': players?.map((player) => player.toJson()).toList(), 'playerIds': (players ?? []).map((player) => player.id).toList(),
'notes': notes, 'notes': notes,
}; };
} }

View File

@@ -21,21 +21,20 @@ class Team {
return 'Team{id: $id, name: $name, members: $members}'; return 'Team{id: $id, name: $name, members: $members}';
} }
/// Creates a Team instance from a JSON object. /// Creates a Team instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
Team.fromJson(Map<String, dynamic> json) Team.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
members = (json['members'] as List) members = []; // Populated during import via DataTransferService
.map((memberJson) => Player.fromJson(memberJson))
.toList();
/// Converts the Team instance to a JSON object. /// Converts the Team instance to a JSON object using normalized format (memberIds only).
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'name': name, 'name': name,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'members': members.map((member) => member.toJson()).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/team.dart';
import 'package:json_schema/json_schema.dart'; import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -17,39 +19,54 @@ class DataTransferService {
static Future<void> deleteAllData(BuildContext context) async { static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
await db.matchDao.deleteAllMatches(); await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups(); await db.groupDao.deleteAllGroups();
await db.gameDao.deleteAllGames();
await db.playerDao.deleteAllPlayers(); await db.playerDao.deleteAllPlayers();
} }
/// Retrieves all application data and converts it to a JSON string. /// Retrieves all application data and converts it to a JSON string.
/// Returns the JSON string representation of the data. /// Returns the JSON string representation of the data in normalized format.
static Future<String> getAppDataAsJson(BuildContext context) async { static Future<String> getAppDataAsJson(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
final matches = await db.matchDao.getAllMatches(); final matches = await db.matchDao.getAllMatches();
final groups = await db.groupDao.getAllGroups(); final groups = await db.groupDao.getAllGroups();
final players = await db.playerDao.getAllPlayers(); final players = await db.playerDao.getAllPlayers();
final games = await db.gameDao.getAllGames();
final teams = await db.teamDao.getAllTeams();
// Construct a JSON representation of the data // Construct a JSON representation of the data in normalized format
final Map<String, dynamic> jsonMap = { final Map<String, dynamic> jsonMap = {
'players': players.map((p) => p.toJson()).toList(), 'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(),
'groups': groups 'groups': groups
.map((g) => { .map((g) => {
'id': g.id, 'id': g.id,
'name': g.name, 'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(), 'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(), 'memberIds': (g.members).map((m) => m.id).toList(),
}).toList(), })
.toList(),
'teams': teams
.map((t) => {
'id': t.id,
'name': t.name,
'createdAt': t.createdAt.toIso8601String(),
'memberIds': (t.members).map((m) => m.id).toList(),
})
.toList(),
'matches': matches 'matches': matches
.map((m) => { .map((m) => {
'id': m.id, 'id': m.id,
'name': m.name, 'name': m.name,
'createdAt': m.createdAt.toIso8601String(), 'createdAt': m.createdAt.toIso8601String(),
'gameId': m.game?.id,
'groupId': m.group?.id, 'groupId': m.group?.id,
'playerIds': (m.players ?? []).map((p) => p.id).toList(), 'playerIds': (m.players ?? []).map((p) => p.id).toList(),
'winnerId': m.winner?.id, 'notes': m.notes,
}).toList(), })
.toList(),
}; };
return json.encode(jsonMap); return json.encode(jsonMap);
@@ -107,10 +124,12 @@ class DataTransferService {
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>; final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>;
final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? []; final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? [];
final List<dynamic> gamesJson = (decoded['games'] as List<dynamic>?) ?? [];
final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? []; final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? [];
final List<dynamic> teamsJson = (decoded['teams'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? []; final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Players // Import Players
final List<Player> importedPlayers = playersJson final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>)) .map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList(); .toList();
@@ -119,7 +138,16 @@ class DataTransferService {
for (final p in importedPlayers) p.id: p, for (final p in importedPlayers) p.id: p,
}; };
// Groups // Import Games
final List<Game> importedGames = gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
final Map<String, Game> gameById = {
for (final g in importedGames) g.id: g,
};
// Import Groups
final List<Group> importedGroups = groupsJson.map((g) { final List<Group> importedGroups = groupsJson.map((g) {
final map = g as Map<String, dynamic>; final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>(); final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
@@ -132,6 +160,7 @@ class DataTransferService {
return Group( return Group(
id: map['id'] as String, id: map['id'] as String,
name: map['name'] as String, name: map['name'] as String,
description: map['description'] as String?,
members: members, members: members,
createdAt: DateTime.parse(map['createdAt'] as String), createdAt: DateTime.parse(map['createdAt'] as String),
); );
@@ -141,33 +170,59 @@ class DataTransferService {
for (final g in importedGroups) g.id: g, for (final g in importedGroups) g.id: g,
}; };
// Matches // Import Teams
final List<Team> importedTeams = teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
final Map<String, Team> teamById = {
for (final t in importedTeams) t.id: t,
};
// Import Matches
final List<Match> importedMatches = matchesJson.map((m) { final List<Match> importedMatches = matchesJson.map((m) {
final map = m as Map<String, dynamic>; final map = m as Map<String, dynamic>;
final String? gameId = map['gameId'] as String?;
final String? groupId = map['groupId'] as String?; final String? groupId = map['groupId'] as String?;
final List<String> playerIds = (map['playerIds'] as List<dynamic>? ?? []).cast<String>(); final List<String> playerIds = (map['playerIds'] as List<dynamic>? ?? []).cast<String>();
final String? winnerId = map['winnerId'] as String?;
final game = (gameId == null) ? null : gameById[gameId];
final group = (groupId == null) ? null : groupById[groupId]; final group = (groupId == null) ? null : groupById[groupId];
final players = playerIds final players = playerIds
.map((id) => playerById[id]) .map((id) => playerById[id])
.whereType<Player>() .whereType<Player>()
.toList(); .toList();
final winner = (winnerId == null) ? null : playerById[winnerId];
return Match( return Match(
id: map['id'] as String, id: map['id'] as String,
name: map['name'] as String, name: map['name'] as String,
game: game,
group: group, group: group,
players: players, players: players.isNotEmpty ? players : null,
createdAt: DateTime.parse(map['createdAt'] as String), createdAt: DateTime.parse(map['createdAt'] as String),
winner: winner, notes: map['notes'] as String?,
); );
}).toList(); }).toList();
// Import all data into the database
await db.playerDao.addPlayersAsList(players: importedPlayers); await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups); await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches); await db.matchDao.addMatchAsList(matches: importedMatches);
return ImportResult.success; return ImportResult.success;