Konsistenzfehler im JSON Vermeiden #125

Merged
flixcoo merged 12 commits from enhancement/70-konsistenzfehler-im-json-vermeiden into development 2026-01-07 11:27:44 +00:00
5 changed files with 192 additions and 204 deletions

View File

@@ -2,178 +2,103 @@
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"games": {
"players": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"players": {
"type": [
"array",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"group": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"members": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
}
},
"required": [
"id",
"createdAt",
"name",
"members"
]
"createdAt": {
"type": "string"
},
"winner": {
"type": ["object","null"]
},
"required": [
"id",
"createdAt",
"name"
]
}
]
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
},
"groups": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"members": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
}
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"required": [
"id",
"createdAt",
"name",
"members"
]
}
]
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"memberIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"name",
"createdAt",
"memberIds"
]
}
},
"players": {
"matches": {
"type": "array",
"items": [
{
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"groupId": {
"anyOf": [
{"type": "string"},
{"type": "null"}
]
},
"playerIds": {
"type": "array",
"items": {
"type": "string"
}
},
"required": [
"id",
"createdAt",
"name"
]
}
]
"winnerId": {
"anyOf": [
{"type": "string"},
{"type": "null"}
]
}
},
"required": [
"id",
"name",
"createdAt",
"groupId",
"playerIds"
]
}
}
}
}
},
"required": [
"players",
"groups",
"matches"
]
}

View File

@@ -95,6 +95,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
}
// Insert unique groups in batch
// Using insertOrIgnore to avoid triggering cascade deletes on
// player_group associations when groups already exist
await db.batch(
(b) => b.insertAll(
groupTable,
@@ -107,7 +109,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
),
)
.toList(),
mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrIgnore,
),
);
@@ -120,6 +122,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
}
if (uniquePlayers.isNotEmpty) {
// Using insertOrIgnore to avoid triggering cascade deletes on
// player_group associations when players already exist
await db.batch(
(b) => b.insertAll(
db.playerTable,
@@ -132,7 +136,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
),
)
.toList(),
mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrIgnore,
),
);
}

View File

@@ -124,6 +124,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
// Add all groups of the matches in batch
// Using insertOrIgnore to avoid overwriting existing groups (which would
// trigger cascade deletes on player_group associations)
await db.batch(
(b) => b.insertAll(
db.groupTable,
@@ -137,7 +139,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
)
.toList(),
mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrIgnore,
),
);
@@ -158,6 +160,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
if (uniquePlayers.isNotEmpty) {
// Using insertOrIgnore to avoid triggering cascade deletes on
// player_group/player_match associations when players already exist
await db.batch(
(b) => b.insertAll(
db.playerTable,
@@ -170,7 +174,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
)
.toList(),
mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrIgnore,
),
);
}

View File

@@ -50,6 +50,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
}
/// Adds multiple [players] to the database in a batch operation.
/// Uses insertOrIgnore to avoid triggering cascade deletes on
/// player_group associations when players already exist.
Future<bool> addPlayersAsList({required List<Player> players}) async {
if (players.isEmpty) return false;
@@ -65,7 +67,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
),
)
.toList(),
mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrIgnore,
),
);

View File

@@ -31,9 +31,25 @@ class DataTransferService {
// Construct a JSON representation of the data
final Map<String, dynamic> jsonMap = {
'matches': matches.map((match) => match.toJson()).toList(),
'groups': groups.map((group) => group.toJson()).toList(),
'players': players.map((player) => player.toJson()).toList(),
'players': players.map((p) => p.toJson()).toList(),
'groups': groups
.map((g) => {
'id': g.id,
'name': g.name,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
}).toList(),
'matches': matches
.map((m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'groupId': m.group?.id,
'playerIds': (m.players ?? []).map((p) => p.id).toList(),
'winnerId': m.winner?.id,
}).toList(),
};
return json.encode(jsonMap);
@@ -46,7 +62,7 @@ class DataTransferService {
/// [fileName] The desired name for the exported file (without extension).
static Future<ExportResult> exportData(
String jsonString,
String fileName,
String fileName
) async {
try {
final bytes = Uint8List.fromList(utf8.encode(jsonString));
@@ -54,11 +70,13 @@ class DataTransferService {
fileName: '$fileName.json',
bytes: bytes,
);
if (path == null) {
return ExportResult.canceled;
} else {
return ExportResult.success;
}
} catch (e, stack) {
print('[exportData] $e');
print(stack);
@@ -81,42 +99,77 @@ class DataTransferService {
try {
final jsonString = await _readFileContent(path.files.single);
if (jsonString == null) {
return ImportResult.fileReadError;
}
if (jsonString == null) return ImportResult.fileReadError;
if (await _validateJsonSchema(jsonString)) {
final Map<String, dynamic> jsonData =
json.decode(jsonString) as Map<String, dynamic>;
final isValid = await _validateJsonSchema(jsonString);
if (!isValid) return ImportResult.invalidSchema;
final List<dynamic>? matchesJson =
jsonData['matches'] as List<dynamic>?;
final List<dynamic>? groupsJson = jsonData['groups'] as List<dynamic>?;
final List<dynamic>? playersJson =
jsonData['players'] as List<dynamic>?;
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>;
gelbeinhalb marked this conversation as resolved
Review

Macht das sinn hier? Ist nicht durch die schema Validierung das schon sichergestellt?

Macht das sinn hier? Ist nicht durch die schema Validierung das schon sichergestellt?
Review

Ja an sich schon, das würde nur greifen, wenn die schema Validierung einen fehler macht.

Ja an sich schon, das würde nur greifen, wenn die schema Validierung einen fehler macht.
final List<Match> importedMatches =
matchesJson
?.map((g) => Match.fromJson(g as Map<String, dynamic>))
.toList() ??
[];
final List<Group> importedGroups =
groupsJson
?.map((g) => Group.fromJson(g as Map<String, dynamic>))
.toList() ??
[];
final List<Player> importedPlayers =
playersJson
?.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList() ??
[];
final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? [];
final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Players
final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
final Map<String, Player> playerById = {
for (final p in importedPlayers) p.id: p,
};
// Groups
final List<Group> importedGroups = groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Group(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
final Map<String, Group> groupById = {
for (final g in importedGroups) g.id: g,
};
// Matches
final List<Match> importedMatches = matchesJson.map((m) {
final map = m as Map<String, dynamic>;
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 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,
group: group,
players: players,
createdAt: DateTime.parse(map['createdAt'] as String),
winner: winner,
);
}).toList();
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.matchDao.addMatchAsList(matches: importedMatches);
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.matchDao.addMatchAsList(matches: importedMatches);
} else {
return ImportResult.invalidSchema;
}
return ImportResult.success;
} on FormatException catch (e, stack) {
print('[importData] FormatException');
@@ -159,4 +212,4 @@ class DataTransferService {
return false;
}
}
}
}