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": {
"type": "string"
},
"description": {
"type": ["string", "null"]
gelbeinhalb marked this conversation as resolved Outdated

Ich glaube es würde mehr Sinn machen, die description nicht nullable zu machen, sondern einfach durch leeren String zu ersetzen, weil wir uns dann viel null checken sparen

Ich glaube es würde mehr Sinn machen, die `description` nicht nullable zu machen, sondern einfach durch leeren String zu ersetzen, weil wir uns dann viel null checken sparen
}
},
"required": [
"id",
"createdAt",
"name"
gelbeinhalb marked this conversation as resolved Outdated

Bei required dann description hinzufügen

Bei `required` dann `description` hinzufügen
]
}
},
"games": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"ruleset": {
"type": ["string", "null"]
gelbeinhalb marked this conversation as resolved Outdated

Ruleset hier auch nicht null, da Game immer ein Ruleset brauch.

Ruleset hier auch nicht null, da Game immer ein Ruleset brauch.
},
"description": {
"type": ["string", "null"]
gelbeinhalb marked this conversation as resolved Outdated

Hier auch wieder description nicht null sondern empty

Hier auch wieder description nicht `null` sondern `empty`
},
"color": {
"type": ["integer", "null"]
gelbeinhalb marked this conversation as resolved Outdated

Warum machst du die color als integer aber die rulesets als string? Ich würde entweder beides als String oder beiden als int (bevorzugt), und beim retrieven dann als enum in den jeweiligen Klassen speichern.

Warum machst du die color als integer aber die rulesets als string? Ich würde entweder beides als `String` oder beiden als `int` (bevorzugt), und beim retrieven dann als `enum` in den jeweiligen Klassen speichern.

war ausversehen 😅😅

war ausversehen 😅😅
},
"icon": {
"type": ["string", "null"]
gelbeinhalb marked this conversation as resolved Outdated

Ich find Icons prinzipell keine schlechte Idee, aber finde sie dann eher bei z.B. Gruppen sinnvoller, weil was für Icons will man bei Games machen? Also bei Gruppen könnte man halt je nach sozialen Aspekten n icon setzen, bei Games fällt mir dazu nicht viel ein

Ich find Icons prinzipell keine schlechte Idee, aber finde sie dann eher bei z.B. Gruppen sinnvoller, weil was für Icons will man bei Games machen? Also bei Gruppen könnte man halt je nach sozialen Aspekten n icon setzen, bei Games fällt mir dazu nicht viel ein

hätte sowas gedacht wie poker karten oder andere icons für die games.
Gruppen icons würde ich auch adden

hätte sowas gedacht wie poker karten oder andere icons für die games. Gruppen icons würde ich auch adden

Das Problem ist, wenn du sagst Pokerkarten als Icon, musst du ja total viele Icons irgendwo her bekommen, die sehr speziell sind (Spielkarten, Brettspiel, etc ..), da würde ich dann vllt eher auf sowas wie emojis setzen und da ist die auswahl die thematisch dazu passt ja begrenzt. Aber sonst kann @sneeex nochmal sagen was er dazu denkt

Das Problem ist, wenn du sagst Pokerkarten als Icon, musst du ja total viele Icons irgendwo her bekommen, die sehr speziell sind (Spielkarten, Brettspiel, etc ..), da würde ich dann vllt eher auf sowas wie emojis setzen und da ist die auswahl die thematisch dazu passt ja begrenzt. Aber sonst kann @sneeex nochmal sagen was er dazu denkt

weiß nicht ob ich emojis geil finde, sehe aber das problem mit den icons

weiß nicht ob ich emojis geil finde, sehe aber das problem mit den icons

lass das sonst demnächst mal besprechen, was man für icons bräuchte

lass das sonst demnächst mal besprechen, was man für icons bräuchte
}
},
"required": [
@@ -25,6 +62,38 @@
}
},
"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",
"items": {
"type": "object",
@@ -67,6 +136,12 @@
"createdAt": {
"type": "string"
},
"gameId": {
gelbeinhalb marked this conversation as resolved Outdated

Meinst du hier matchId?

Meinst du hier `matchId`?

bin gerade am handy, gucke später

bin gerade am handy, gucke später

ja meinte ich. Ist aber jetzt komplett aus dem Team array entfernt

ja meinte ich. Ist aber jetzt komplett aus dem Team array entfernt
"anyOf": [
gelbeinhalb marked this conversation as resolved Outdated

Warum hier nicht

"type": ["string", "null"]

Würde das konsequent machen, eins von beiden und dann das gesamte Schema durch.

Warum hier nicht ```json "type": ["string", "null"] ``` Würde das konsequent machen, eins von beiden und dann das gesamte Schema durch.
{"type": "string"},
{"type": "null"}
gelbeinhalb marked this conversation as resolved Outdated

Wenn es hier um ein Match und nicht um ein Game geht, darf Match(Game) nicht null sein, weil ein Spiel immer eine Spielvorlage haben muss

Wenn es hier um ein `Match` und nicht um ein `Game` geht, darf `Match`(`Game`) nicht `null` sein, weil ein Spiel immer eine Spielvorlage haben muss
]
},
"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": [
{"type": "string"},
@@ -79,7 +154,7 @@
"type": "string"
}
},
"winnerId": {
"notes": {
"anyOf": [
{"type": "string"},
{"type": "null"}
@@ -90,7 +165,6 @@
"id",
"name",
"createdAt",
"groupId",
"playerIds"
]
}
@@ -98,7 +172,9 @@
},
"required": [
"players",
"games",
"groups",
"teams",
"matches"
]
}

View File

@@ -23,22 +23,21 @@ class Group {
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)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
description = json['description'],
members = (json['members'] as List)
.map((memberJson) => Player.fromJson(memberJson))
.toList();
members = []; // Populated during import via DataTransferService
/// 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() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'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
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)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
game = json['game'] != null ? Game.fromJson(json['game']) : null,
group = json['group'] != null ? Group.fromJson(json['group']) : null,
players = json['players'] != null
? (json['players'] as List)
.map((playerJson) => Player.fromJson(playerJson))
.toList()
: null,
game = null, // Populated during import via DataTransferService
group = null, // Populated during import via DataTransferService
players = [], // Populated during import via DataTransferService
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() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'game': game?.toJson(),
'group': group?.toJson(),
'players': players?.map((player) => player.toJson()).toList(),
'gameId': game?.id,
'groupId': group?.id,
'playerIds': (players ?? []).map((player) => player.id).toList(),
'notes': notes,
};
}

View File

@@ -21,21 +21,20 @@ class Team {
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)
: id = json['id'],
name = json['name'],
createdAt = DateTime.parse(json['createdAt']),
members = (json['members'] as List)
.map((memberJson) => Player.fromJson(memberJson))
.toList();
members = []; // Populated during import via DataTransferService
/// 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() => {
'id': id,
'name': name,
'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:game_tracker/core/enums.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/match.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:provider/provider.dart';
@@ -17,39 +19,54 @@ class DataTransferService {
static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups();
await db.gameDao.deleteAllGames();
await db.playerDao.deleteAllPlayers();
}
/// 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 {
final db = Provider.of<AppDatabase>(context, listen: false);
final matches = await db.matchDao.getAllMatches();
final groups = await db.groupDao.getAllGroups();
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 = {
'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(),
'groups': groups
.map((g) => {
'id': g.id,
'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(),
'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
.map((m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'gameId': m.game?.id,
'groupId': m.group?.id,
'playerIds': (m.players ?? []).map((p) => p.id).toList(),
'winnerId': m.winner?.id,
}).toList(),
'notes': m.notes,
})
.toList(),
};
return json.encode(jsonMap);
@@ -107,10 +124,12 @@ class DataTransferService {
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, 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> teamsJson = (decoded['teams'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Players
// Import Players
final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
@@ -119,7 +138,16 @@ class DataTransferService {
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 map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
@@ -132,6 +160,7 @@ class DataTransferService {
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String?,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
@@ -141,33 +170,59 @@ class DataTransferService {
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 map = m as Map<String, dynamic>;
final String? gameId = map['gameId'] as String?;
final String? groupId = map['groupId'] as 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 players = playerIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
final winner = (winnerId == null) ? null : playerById[winnerId];
return Match(
id: map['id'] as String,
name: map['name'] as String,
game: game,
group: group,
players: players,
players: players.isNotEmpty ? players : null,
createdAt: DateTime.parse(map['createdAt'] as String),
winner: winner,
notes: map['notes'] as String?,
);
}).toList();
// Import all data into the database
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
return ImportResult.success;