database: Unify work and recording API

This commit is contained in:
Elias Projahn 2020-04-26 16:48:05 +02:00
parent c93ebf17a0
commit 0fc0c933ac
7 changed files with 116 additions and 201 deletions

View file

@ -82,11 +82,11 @@ class MusicusClient {
/// Create or update a work. /// Create or update a work.
/// ///
/// The new or updated work is returned. /// The new or updated work is returned.
Future<WorkInfo> putWork(WorkData data) async { Future<void> putWork(WorkInfo workInfo) async {
final response = await _client.put( final response = await _client.put(
'$host/works/${data.data.work.id}', '$host/works/${workInfo.work.id}',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(data.toJson()), body: jsonEncode(workInfo.toJson()),
); );
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
@ -124,17 +124,12 @@ class MusicusClient {
} }
/// Create or update a recording. /// Create or update a recording.
/// Future<void> putRecording(RecordingInfo recordingInfo) async {
/// The new or updated recording is returned. await _client.put(
Future<RecordingInfo> putRecording(RecordingData data) async { '$host/recordings/${recordingInfo.recording.id}',
final response = await _client.put(
'$host/recordings/${data.recording.id}',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(data.toJson()), body: jsonEncode(recordingInfo.toJson()),
); );
final json = jsonDecode(response.body);
return RecordingInfo.fromJson(json);
} }
/// Close the internal http client. /// Close the internal http client.

View file

@ -9,70 +9,6 @@ part 'database.g.dart';
final _random = Random(DateTime.now().millisecondsSinceEpoch); final _random = Random(DateTime.now().millisecondsSinceEpoch);
int generateId() => _random.nextInt(0xFFFFFFFF); int generateId() => _random.nextInt(0xFFFFFFFF);
class WorkPartData {
final Work work;
final List<int> instrumentIds;
WorkPartData({
this.work,
this.instrumentIds,
});
factory WorkPartData.fromJson(Map<String, dynamic> json) => WorkPartData(
work: Work.fromJson(json['work']),
instrumentIds: List<int>.from(json['instrumentIds']),
);
Map<String, dynamic> toJson() => {
'work': work.toJson(),
'instrumentIds': instrumentIds,
};
}
class WorkData {
final WorkPartData data;
final List<WorkPartData> partData;
WorkData({
this.data,
this.partData,
});
factory WorkData.fromJson(Map<String, dynamic> json) => WorkData(
data: WorkPartData.fromJson(json['data']),
partData: json['partData']
.map<WorkPartData>((j) => WorkPartData.fromJson(j))
.toList(),
);
Map<String, dynamic> toJson() => {
'data': data.toJson(),
'partData': partData.map((d) => d.toJson()).toList(),
};
}
class RecordingData {
final Recording recording;
final List<Performance> performances;
RecordingData({
this.recording,
this.performances,
});
factory RecordingData.fromJson(Map<String, dynamic> json) => RecordingData(
recording: Recording.fromJson(json['recording']),
performances: json['performances']
.map<Performance>((j) => Performance.fromJson(j))
.toList(),
);
Map<String, dynamic> toJson() => {
'recording': recording.toJson(),
'performances': performances.map((p) => p.toJson()).toList(),
};
}
@UseMoor( @UseMoor(
include: { include: {
'database.moor', 'database.moor',
@ -145,25 +81,44 @@ class Database extends _$Database {
return result; return result;
} }
Future<void> updateWork(WorkData data) async { /// Update a work and its associated data.
///
/// This will explicitly update all associated composers and instruments, even
/// if they have already existed before.
Future<void> updateWork(WorkInfo workInfo) async {
await transaction(() async { await transaction(() async {
final workId = data.data.work.id; final workId = workInfo.work.id;
await (delete(works)..where((w) => w.id.equals(workId))).go();
await (delete(works)..where((w) => w.partOf.equals(workId))).go();
Future<void> insertWork(WorkPartData partData) async { // Delete old work data first. The parts and instrumentations will be
await into(works).insert(partData.work); // deleted automatically due to their foreign key constraints.
await batch((b) => b.insertAll( await (delete(works)..where((w) => w.id.equals(workId))).go();
instrumentations,
partData.instrumentIds /// Insert instrumentations for a work.
.map((id) => ///
Instrumentation(work: partData.work.id, instrument: id)) /// At the moment, this will also update all provided instruments, even
.toList())); /// if they were already there previously.
Future<void> insertInstrumentations(
int workId, List<Instrument> instruments) async {
for (final instrument in instruments) {
await updateInstrument(instrument);
await into(instrumentations).insert(Instrumentation(
work: workId,
instrument: instrument.id,
));
}
} }
await insertWork(data.data); // This will also include the composers of the work's parts.
for (final partData in data.partData) { for (final person in workInfo.composers) {
await insertWork(partData); await updatePerson(person);
}
await into(works).insert(workInfo.work);
await insertInstrumentations(workId, workInfo.instruments);
for (final partInfo in workInfo.parts) {
await into(works).insert(partInfo.work);
await insertInstrumentations(partInfo.work.id, partInfo.instruments);
} }
}); });
} }
@ -172,14 +127,38 @@ class Database extends _$Database {
await into(ensembles).insert(ensemble, orReplace: true); await into(ensembles).insert(ensemble, orReplace: true);
} }
Future<void> updateRecording(RecordingData data) async { /// Update a recording and its associated data.
///
/// This will explicitly also update all assoicated persons and instruments.
Future<void> updateRecording(RecordingInfo recordingInfo) async {
await transaction(() async { await transaction(() async {
await (delete(performances) final recordingId = recordingInfo.recording.id;
..where((p) => p.recording.equals(data.recording.id)))
.go(); // Delete the old recording first. This will also delete the performances
await into(recordings).insert(data.recording, orReplace: true); // due to their foreign key constraint.
for (final performance in data.performances) { await (delete(recordings)..where((r) => r.id.equals(recordingId))).go();
await into(performances).insert(performance);
await into(recordings).insert(recordingInfo.recording);
for (final performance in recordingInfo.performances) {
if (performance.person != null) {
await updatePerson(performance.person);
}
if (performance.ensemble != null) {
await updateEnsemble(performance.ensemble);
}
if (performance.role != null) {
await updateInstrument(performance.role);
}
await into(performances).insert(Performance(
recording: recordingId,
person: performance.person?.id,
ensemble: performance.ensemble?.id,
role: performance.role?.id,
));
} }
}); });
} }

View file

@ -98,31 +98,24 @@ class _RecordingEditorState extends State<RecordingEditor> {
FlatButton( FlatButton(
child: Text('DONE'), child: Text('DONE'),
onPressed: () async { onPressed: () async {
final recording = Recording( final recordingInfo = RecordingInfo(
recording: Recording(
id: widget.recording?.id ?? generateId(), id: widget.recording?.id ?? generateId(),
work: workInfo.work.id, work: workInfo.work.id,
comment: commentController.text, comment: commentController.text,
),
performances: performanceInfos,
); );
final performances = performanceInfos await backend.client.putRecording(recordingInfo);
.map((m) => Performance(
recording: recording.id,
person: m.person?.id,
ensemble: m.ensemble?.id,
role: m.role?.id,
))
.toList();
final recordingInfo = Navigator.pop(
await backend.client.putRecording(RecordingData( context,
recording: recording, RecordingSelectorResult(
performances: performances,
));
Navigator.pop(context, RecordingSelectorResult(
workInfo: workInfo, workInfo: workInfo,
recordingInfo: recordingInfo, recordingInfo: recordingInfo,
)); ),
);
}, },
) )
], ],

View file

@ -54,62 +54,8 @@ class _TracksEditorState extends State<TracksEditor> {
// We need to copy all information associated with this track we // We need to copy all information associated with this track we
// got by asking the server to our local database. For now, we // got by asking the server to our local database. For now, we
// will just override everything that we already had previously. // will just override everything that we already had previously.
backend.db.updateWork(workInfo);
// TODO: Think about efficiency. backend.db.updateRecording(recordingInfo);
backend.db.transaction(() async {
for (final composer in workInfo.composers) {
await backend.db.updatePerson(composer);
}
for (final instrument in workInfo.instruments) {
await backend.db.updateInstrument(instrument);
}
for (final partInfo in workInfo.parts) {
for (final instrument in partInfo.instruments) {
await backend.db.updateInstrument(instrument);
}
}
await backend.db.updateWork(WorkData(
data: WorkPartData(
work: workInfo.work,
instrumentIds:
workInfo.instruments.map((i) => i.id).toList(),
),
partData: workInfo.parts
.map((p) => WorkPartData(
work: p.work,
instrumentIds:
p.instruments.map((i) => i.id).toList(),
))
.toList(),
));
for (final performance in recordingInfo.performances) {
if (performance.person != null) {
await backend.db.updatePerson(performance.person);
}
if (performance.ensemble != null) {
await backend.db.updateEnsemble(performance.ensemble);
}
if (performance.role != null) {
await backend.db.updateInstrument(performance.role);
}
}
await backend.db.updateRecording(RecordingData(
recording: recordingInfo.recording,
performances: recordingInfo.performances
.map((p) => Performance(
recording: recordingInfo.recording.id,
person: p.person?.id,
ensemble: p.ensemble?.id,
role: p.role?.id,
))
.toList(),
));
});
backend.ml.addTracks(parentId, tracks); backend.ml.addTracks(parentId, tracks);

View file

@ -299,19 +299,10 @@ class _WorkEditorState extends State<WorkEditor> {
onPressed: () async { onPressed: () async {
final workId = widget.work?.id ?? generateId(); final workId = widget.work?.id ?? generateId();
final data = WorkPartData( List<PartInfo> partInfos = [];
work: Work(
id: workId,
title: titleController.text,
composer: composer?.id,
),
instrumentIds: instruments.map((i) => i.id).toList(),
);
final List<WorkPartData> partData = [];
for (var i = 0; i < parts.length; i++) { for (var i = 0; i < parts.length; i++) {
final part = parts[i]; final part = parts[i];
partData.add(WorkPartData( partInfos.add(PartInfo(
work: Work( work: Work(
id: generateId(), id: generateId(),
title: part.titleController.text, title: part.titleController.text,
@ -319,14 +310,25 @@ class _WorkEditorState extends State<WorkEditor> {
partOf: workId, partOf: workId,
partIndex: i, partIndex: i,
), ),
instrumentIds: part.instruments.map((i) => i.id).toList(), instruments: part.instruments,
composer: part.composer,
)); ));
} }
final workInfo = await backend.client.putWork(WorkData( final workInfo = WorkInfo(
data: data, work: Work(
partData: partData, id: workId,
)); title: titleController.text,
composer: composer?.id,
),
instruments: instruments,
// TODO: Theoretically, this should include all composers from
// the parts.
composers: [composer],
parts: partInfos,
);
await backend.client.putWork(workInfo);
Navigator.pop(context, workInfo); Navigator.pop(context, workInfo);
}, },

View file

@ -19,9 +19,9 @@ class RecordingsController extends ResourceController {
@Operation.put('id') @Operation.put('id')
Future<Response> putRecording( Future<Response> putRecording(
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> json) async { @Bind.path('id') int id, @Bind.body() Map<String, dynamic> json) async {
final data = RecordingData.fromJson(json); final recordingInfo = RecordingInfo.fromJson(json);
await db.updateRecording(data); await db.updateRecording(recordingInfo);
return Response.ok(await db.getRecording(id)); return Response.ok(null);
} }
} }

View file

@ -19,9 +19,9 @@ class WorksController extends ResourceController {
@Operation.put('id') @Operation.put('id')
Future<Response> putWork( Future<Response> putWork(
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> json) async { @Bind.path('id') int id, @Bind.body() Map<String, dynamic> json) async {
final data = WorkData.fromJson(json); final workInfo = WorkInfo.fromJson(json);
await db.updateWork(data); await db.updateWork(workInfo);
return Response.ok(await db.getWork(id)); return Response.ok(null);
} }
} }