mirror of
https://github.com/immich-app/immich.git
synced 2026-04-23 17:49:44 -04:00
chore!: migrate album owner to album_user (#27467)
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
dfacde5af8
commit
4bfb8b36c2
@ -154,23 +154,31 @@ describe('/albums', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedLink,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user2.userId,
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
]),
|
||||
@ -184,23 +192,31 @@ describe('/albums', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedLink,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1NotShared,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
@ -216,23 +232,31 @@ describe('/albums', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1SharedLink,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user2.userId,
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||
]),
|
||||
shared: true,
|
||||
}),
|
||||
]),
|
||||
@ -248,8 +272,10 @@ describe('/albums', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user1.userId,
|
||||
albumName: user1NotShared,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||
]),
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
@ -286,13 +312,17 @@ describe('/albums', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4DeletedAsset,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
||||
]),
|
||||
shared: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4Empty,
|
||||
albumUsers: expect.arrayContaining([
|
||||
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
||||
]),
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
@ -362,16 +392,17 @@ describe('/albums', () => {
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user2Albums[0],
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -397,15 +428,13 @@ describe('/albums', () => {
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
ownerId: user1.userId,
|
||||
albumName: 'New album',
|
||||
description: '',
|
||||
albumThumbnailAssetId: null,
|
||||
shared: false,
|
||||
albumUsers: [],
|
||||
albumUsers: [{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) }],
|
||||
hasSharedLink: false,
|
||||
assetCount: 0,
|
||||
owner: expect.objectContaining({ email: user1.userEmail }),
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
});
|
||||
@ -621,11 +650,11 @@ describe('/albums', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
albumUsers: [
|
||||
albumUsers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
user: expect.objectContaining({ id: user2.userId }),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -637,7 +666,7 @@ describe('/albums', () => {
|
||||
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
|
||||
expect(body).toEqual(errorDto.badRequest('User already added'));
|
||||
});
|
||||
|
||||
it('should not be able to add existing user to shared album', async () => {
|
||||
@ -663,7 +692,7 @@ describe('/albums', () => {
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
});
|
||||
|
||||
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
|
||||
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
||||
|
||||
const { status } = await request(app)
|
||||
.put(`/albums/${album.id}/user/${user2.userId}`)
|
||||
@ -678,7 +707,10 @@ describe('/albums', () => {
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
|
||||
albumUsers: [
|
||||
expect.objectContaining({ role: AlbumUserRole.Owner }),
|
||||
expect.objectContaining({ role: AlbumUserRole.Editor }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -689,7 +721,7 @@ describe('/albums', () => {
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
});
|
||||
|
||||
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
|
||||
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/albums/${album.id}/user/${user2.userId}`)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
UserAvatarColor,
|
||||
@ -420,9 +421,7 @@ export function getAlbum(
|
||||
albumThumbnailAssetId: album.thumbnailAssetId,
|
||||
createdAt: album.createdAt,
|
||||
updatedAt: album.updatedAt,
|
||||
ownerId: albumOwner.id,
|
||||
owner: albumOwner,
|
||||
albumUsers: [], // Empty array for non-shared album
|
||||
albumUsers: [{ user: albumOwner, role: AlbumUserRole.Owner }],
|
||||
shared: false,
|
||||
hasSharedLink: false,
|
||||
isActivityEnabled: true,
|
||||
|
||||
3301
mobile/drift_schemas/main/drift_schema_v24.json
generated
Normal file
3301
mobile/drift_schemas/main/drift_schema_v24.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ enum AlbumUserRole {
|
||||
// do not change this order!
|
||||
editor,
|
||||
viewer,
|
||||
owner,
|
||||
}
|
||||
|
||||
// Model for an album stored in the server
|
||||
|
||||
@ -7,8 +7,8 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
|
||||
class RemoteAlbumService {
|
||||
final DriftRemoteAlbumRepository _repository;
|
||||
@ -28,10 +28,6 @@ class RemoteAlbumService {
|
||||
return _repository.get(albumId);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||
return _repository.getByName(albumName, ownerId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
AlbumSortMode sortMode, {
|
||||
@ -86,8 +82,18 @@ class RemoteAlbumService {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
||||
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
||||
Future<RemoteAlbum> createAlbum({
|
||||
required String title,
|
||||
required UserDto owner,
|
||||
required List<String> assetIds,
|
||||
String? description,
|
||||
}) async {
|
||||
final album = await _albumApiRepository.createDriftAlbum(
|
||||
title,
|
||||
owner,
|
||||
description: description,
|
||||
assetIds: assetIds,
|
||||
);
|
||||
await _repository.create(album, assetIds);
|
||||
|
||||
return album;
|
||||
@ -101,8 +107,10 @@ class RemoteAlbumService {
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
}) async {
|
||||
final owner = await _repository.getOwner(albumId);
|
||||
final updatedAlbum = await _albumApiRepository.updateAlbum(
|
||||
albumId,
|
||||
owner,
|
||||
name: name,
|
||||
description: description,
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@ -12,6 +15,7 @@ final syncLinkedAlbumServiceProvider = Provider(
|
||||
ref.watch(localAlbumRepository),
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
ref.watch(storeServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@ -19,8 +23,14 @@ class SyncLinkedAlbumService {
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final StoreService _storeService;
|
||||
|
||||
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
SyncLinkedAlbumService(
|
||||
this._localAlbumRepository,
|
||||
this._remoteAlbumRepository,
|
||||
this._albumApiRepository,
|
||||
this._storeService,
|
||||
);
|
||||
|
||||
final _log = Logger("SyncLinkedAlbumService");
|
||||
|
||||
@ -103,7 +113,11 @@ class SyncLinkedAlbumService {
|
||||
/// Creates a new remote album and links it to the local album
|
||||
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
|
||||
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
|
||||
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
|
||||
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(
|
||||
localAlbum.name,
|
||||
_storeService.get(StoreKey.currentUser),
|
||||
assetIds: [],
|
||||
);
|
||||
await _remoteAlbumRepository.create(newRemoteAlbum, []);
|
||||
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
|
||||
}
|
||||
|
||||
@ -225,6 +225,8 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'partner backfill');
|
||||
case SyncEntityType.albumV1:
|
||||
return _syncStreamRepository.updateAlbumsV1(data.cast());
|
||||
case SyncEntityType.albumV2:
|
||||
return _syncStreamRepository.updateAlbumsV2(data.cast());
|
||||
case SyncEntityType.albumDeleteV1:
|
||||
return _syncStreamRepository.deleteAlbumsV1(data.cast());
|
||||
case SyncEntityType.albumUserV1:
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)')
|
||||
class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
const RemoteAlbumEntity();
|
||||
|
||||
@ -18,8 +16,6 @@ class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get thumbnailAssetId =>
|
||||
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()();
|
||||
|
||||
|
||||
@ -7,11 +7,9 @@ import 'package:immich_mobile/domain/models/album/album.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'
|
||||
as i3;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:drift/internal/modular.dart' as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i7;
|
||||
|
||||
typedef $$RemoteAlbumEntityTableCreateCompanionBuilder =
|
||||
i1.RemoteAlbumEntityCompanion Function({
|
||||
@ -20,7 +18,6 @@ typedef $$RemoteAlbumEntityTableCreateCompanionBuilder =
|
||||
i0.Value<String> description,
|
||||
i0.Value<DateTime> createdAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
required String ownerId,
|
||||
i0.Value<String?> thumbnailAssetId,
|
||||
i0.Value<bool> isActivityEnabled,
|
||||
required i2.AlbumAssetOrder order,
|
||||
@ -32,7 +29,6 @@ typedef $$RemoteAlbumEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<String> description,
|
||||
i0.Value<DateTime> createdAt,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<String> ownerId,
|
||||
i0.Value<String?> thumbnailAssetId,
|
||||
i0.Value<bool> isActivityEnabled,
|
||||
i0.Value<i2.AlbumAssetOrder> order,
|
||||
@ -51,42 +47,10 @@ final class $$RemoteAlbumEntityTableReferences
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$UserEntityTable>('user_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$RemoteAlbumEntityTable>('remote_album_entity')
|
||||
.ownerId,
|
||||
i6.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i5.$$UserEntityTableProcessedTableManager get ownerId {
|
||||
final $_column = $_itemColumn<String>('owner_id')!;
|
||||
|
||||
final manager = i5
|
||||
.$$UserEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]),
|
||||
);
|
||||
}
|
||||
|
||||
static i7.$RemoteAssetEntityTable _thumbnailAssetIdTable(
|
||||
static i5.$RemoteAssetEntityTable _thumbnailAssetIdTable(
|
||||
i0.GeneratedDatabase db,
|
||||
) => i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
@ -94,19 +58,19 @@ final class $$RemoteAlbumEntityTableReferences
|
||||
.thumbnailAssetId,
|
||||
i6.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i7.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {
|
||||
i5.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId {
|
||||
final $_column = $_itemColumn<String>('thumbnail_asset_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = i7
|
||||
final manager = i5
|
||||
.$$RemoteAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_thumbnailAssetIdTable($_db));
|
||||
@ -162,51 +126,24 @@ class $$RemoteAlbumEntityTableFilterComposer
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$UserEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
|
||||
i7.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId {
|
||||
final i7.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
i5.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId {
|
||||
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i7.$$RemoteAssetEntityTableFilterComposer(
|
||||
}) => i5.$$RemoteAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@ -261,52 +198,25 @@ class $$RemoteAlbumEntityTableOrderingComposer
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$UserEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
|
||||
i7.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId {
|
||||
final i7.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
i5.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId {
|
||||
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i7.$$RemoteAssetEntityTableOrderingComposer(
|
||||
}) => i5.$$RemoteAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@ -351,52 +261,25 @@ class $$RemoteAlbumEntityTableAnnotationComposer
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AlbumAssetOrder, int> get order =>
|
||||
$composableBuilder(column: $table.order, builder: (column) => column);
|
||||
|
||||
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.ownerId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$UserEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$UserEntityTable>('user_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
|
||||
i7.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId {
|
||||
final i7.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
i5.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId {
|
||||
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailAssetId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i7.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
}) => i5.$$RemoteAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i7.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
@ -420,7 +303,7 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
|
||||
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
|
||||
i1.RemoteAlbumEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})
|
||||
i0.PrefetchHooks Function({bool thumbnailAssetId})
|
||||
> {
|
||||
$$RemoteAlbumEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
@ -445,7 +328,6 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
i0.Value<String> description = const i0.Value.absent(),
|
||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
i0.Value<String> ownerId = const i0.Value.absent(),
|
||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
||||
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
|
||||
i0.Value<i2.AlbumAssetOrder> order = const i0.Value.absent(),
|
||||
@ -455,7 +337,6 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order,
|
||||
@ -467,7 +348,6 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
i0.Value<String> description = const i0.Value.absent(),
|
||||
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
required String ownerId,
|
||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
||||
i0.Value<bool> isActivityEnabled = const i0.Value.absent(),
|
||||
required i2.AlbumAssetOrder order,
|
||||
@ -477,7 +357,6 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order,
|
||||
@ -490,7 +369,7 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({ownerId = false, thumbnailAssetId = false}) {
|
||||
prefetchHooksCallback: ({thumbnailAssetId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
@ -510,21 +389,6 @@ class $$RemoteAlbumEntityTableTableManager
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (ownerId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.ownerId,
|
||||
referencedTable: i1
|
||||
.$$RemoteAlbumEntityTableReferences
|
||||
._ownerIdTable(db),
|
||||
referencedColumn: i1
|
||||
.$$RemoteAlbumEntityTableReferences
|
||||
._ownerIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
if (thumbnailAssetId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
@ -564,12 +428,8 @@ typedef $$RemoteAlbumEntityTableProcessedTableManager =
|
||||
$$RemoteAlbumEntityTableUpdateCompanionBuilder,
|
||||
(i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences),
|
||||
i1.RemoteAlbumEntityData,
|
||||
i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})
|
||||
i0.PrefetchHooks Function({bool thumbnailAssetId})
|
||||
>;
|
||||
i0.Index get idxRemoteAlbumOwnerId => i0.Index(
|
||||
'idx_remote_album_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
|
||||
);
|
||||
|
||||
class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
||||
with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> {
|
||||
@ -636,20 +496,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i4.currentDateAndTime,
|
||||
);
|
||||
static const i0.VerificationMeta _ownerIdMeta = const i0.VerificationMeta(
|
||||
'ownerId',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
||||
'owner_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES user_entity (id) ON DELETE CASCADE',
|
||||
),
|
||||
);
|
||||
static const i0.VerificationMeta _thumbnailAssetIdMeta =
|
||||
const i0.VerificationMeta('thumbnailAssetId');
|
||||
@override
|
||||
@ -698,7 +544,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
||||
description,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
ownerId,
|
||||
thumbnailAssetId,
|
||||
isActivityEnabled,
|
||||
order,
|
||||
@ -749,14 +594,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('owner_id')) {
|
||||
context.handle(
|
||||
_ownerIdMeta,
|
||||
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_ownerIdMeta);
|
||||
}
|
||||
if (data.containsKey('thumbnail_asset_id')) {
|
||||
context.handle(
|
||||
_thumbnailAssetIdMeta,
|
||||
@ -807,10 +644,6 @@ class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity
|
||||
i0.DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
ownerId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}owner_id'],
|
||||
)!,
|
||||
thumbnailAssetId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}thumbnail_asset_id'],
|
||||
@ -850,7 +683,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
final String description;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String ownerId;
|
||||
final String? thumbnailAssetId;
|
||||
final bool isActivityEnabled;
|
||||
final i2.AlbumAssetOrder order;
|
||||
@ -860,7 +692,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
required this.description,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.ownerId,
|
||||
this.thumbnailAssetId,
|
||||
required this.isActivityEnabled,
|
||||
required this.order,
|
||||
@ -873,7 +704,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
map['description'] = i0.Variable<String>(description);
|
||||
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||
if (!nullToAbsent || thumbnailAssetId != null) {
|
||||
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId);
|
||||
}
|
||||
@ -897,7 +727,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||
thumbnailAssetId: serializer.fromJson<String?>(json['thumbnailAssetId']),
|
||||
isActivityEnabled: serializer.fromJson<bool>(json['isActivityEnabled']),
|
||||
order: i1.$RemoteAlbumEntityTable.$converterorder.fromJson(
|
||||
@ -914,7 +743,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
'description': serializer.toJson<String>(description),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'ownerId': serializer.toJson<String>(ownerId),
|
||||
'thumbnailAssetId': serializer.toJson<String?>(thumbnailAssetId),
|
||||
'isActivityEnabled': serializer.toJson<bool>(isActivityEnabled),
|
||||
'order': serializer.toJson<int>(
|
||||
@ -929,7 +757,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? ownerId,
|
||||
i0.Value<String?> thumbnailAssetId = const i0.Value.absent(),
|
||||
bool? isActivityEnabled,
|
||||
i2.AlbumAssetOrder? order,
|
||||
@ -939,7 +766,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
thumbnailAssetId: thumbnailAssetId.present
|
||||
? thumbnailAssetId.value
|
||||
: this.thumbnailAssetId,
|
||||
@ -955,7 +781,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
: this.description,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||
thumbnailAssetId: data.thumbnailAssetId.present
|
||||
? data.thumbnailAssetId.value
|
||||
: this.thumbnailAssetId,
|
||||
@ -974,7 +799,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
..write('description: $description, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('ownerId: $ownerId, ')
|
||||
..write('thumbnailAssetId: $thumbnailAssetId, ')
|
||||
..write('isActivityEnabled: $isActivityEnabled, ')
|
||||
..write('order: $order')
|
||||
@ -989,7 +813,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
description,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
ownerId,
|
||||
thumbnailAssetId,
|
||||
isActivityEnabled,
|
||||
order,
|
||||
@ -1003,7 +826,6 @@ class RemoteAlbumEntityData extends i0.DataClass
|
||||
other.description == this.description &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.ownerId == this.ownerId &&
|
||||
other.thumbnailAssetId == this.thumbnailAssetId &&
|
||||
other.isActivityEnabled == this.isActivityEnabled &&
|
||||
other.order == this.order);
|
||||
@ -1016,7 +838,6 @@ class RemoteAlbumEntityCompanion
|
||||
final i0.Value<String> description;
|
||||
final i0.Value<DateTime> createdAt;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
final i0.Value<String> ownerId;
|
||||
final i0.Value<String?> thumbnailAssetId;
|
||||
final i0.Value<bool> isActivityEnabled;
|
||||
final i0.Value<i2.AlbumAssetOrder> order;
|
||||
@ -1026,7 +847,6 @@ class RemoteAlbumEntityCompanion
|
||||
this.description = const i0.Value.absent(),
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
this.ownerId = const i0.Value.absent(),
|
||||
this.thumbnailAssetId = const i0.Value.absent(),
|
||||
this.isActivityEnabled = const i0.Value.absent(),
|
||||
this.order = const i0.Value.absent(),
|
||||
@ -1037,13 +857,11 @@ class RemoteAlbumEntityCompanion
|
||||
this.description = const i0.Value.absent(),
|
||||
this.createdAt = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
required String ownerId,
|
||||
this.thumbnailAssetId = const i0.Value.absent(),
|
||||
this.isActivityEnabled = const i0.Value.absent(),
|
||||
required i2.AlbumAssetOrder order,
|
||||
}) : id = i0.Value(id),
|
||||
name = i0.Value(name),
|
||||
ownerId = i0.Value(ownerId),
|
||||
order = i0.Value(order);
|
||||
static i0.Insertable<i1.RemoteAlbumEntityData> custom({
|
||||
i0.Expression<String>? id,
|
||||
@ -1051,7 +869,6 @@ class RemoteAlbumEntityCompanion
|
||||
i0.Expression<String>? description,
|
||||
i0.Expression<DateTime>? createdAt,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
i0.Expression<String>? ownerId,
|
||||
i0.Expression<String>? thumbnailAssetId,
|
||||
i0.Expression<bool>? isActivityEnabled,
|
||||
i0.Expression<int>? order,
|
||||
@ -1062,7 +879,6 @@ class RemoteAlbumEntityCompanion
|
||||
if (description != null) 'description': description,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (ownerId != null) 'owner_id': ownerId,
|
||||
if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId,
|
||||
if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled,
|
||||
if (order != null) 'order': order,
|
||||
@ -1075,7 +891,6 @@ class RemoteAlbumEntityCompanion
|
||||
i0.Value<String>? description,
|
||||
i0.Value<DateTime>? createdAt,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
i0.Value<String>? ownerId,
|
||||
i0.Value<String?>? thumbnailAssetId,
|
||||
i0.Value<bool>? isActivityEnabled,
|
||||
i0.Value<i2.AlbumAssetOrder>? order,
|
||||
@ -1086,7 +901,6 @@ class RemoteAlbumEntityCompanion
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
|
||||
order: order ?? this.order,
|
||||
@ -1111,9 +925,6 @@ class RemoteAlbumEntityCompanion
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
if (ownerId.present) {
|
||||
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||
}
|
||||
if (thumbnailAssetId.present) {
|
||||
map['thumbnail_asset_id'] = i0.Variable<String>(thumbnailAssetId.value);
|
||||
}
|
||||
@ -1136,7 +947,6 @@ class RemoteAlbumEntityCompanion
|
||||
..write('description: $description, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('ownerId: $ownerId, ')
|
||||
..write('thumbnailAssetId: $thumbnailAssetId, ')
|
||||
..write('isActivityEnabled: $isActivityEnabled, ')
|
||||
..write('order: $order')
|
||||
|
||||
@ -84,7 +84,7 @@ class Drift extends $Drift {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 23;
|
||||
int get schemaVersion => 24;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@ -246,6 +246,10 @@ class Drift extends $Drift {
|
||||
),
|
||||
);
|
||||
},
|
||||
from23To24: (m, v24) async {
|
||||
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
||||
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -105,7 +105,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
i7.idxLocalAlbumAssetAlbumAsset,
|
||||
i5.idxRemoteAlbumOwnerId,
|
||||
i4.idxLocalAssetChecksum,
|
||||
i4.idxLocalAssetCloudId,
|
||||
i3.idxStackPrimaryAssetId,
|
||||
@ -160,15 +159,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
),
|
||||
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
|
||||
@ -11824,6 +11824,557 @@ i1.GeneratedColumn<int> _column_209(String aliasedName) =>
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
final class Schema24 extends i0.VersionedSchema {
|
||||
Schema24({required super.database}) : super(version: 24);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxStackPrimaryAssetId,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetLocalDateTimeDay,
|
||||
idxRemoteAssetLocalDateTimeMonth,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
idxAssetEditAssetId,
|
||||
];
|
||||
late final Shape33 userEntity = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape34 remoteAssetEntity = Shape34(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_119,
|
||||
_column_120,
|
||||
_column_121,
|
||||
_column_122,
|
||||
_column_123,
|
||||
_column_124,
|
||||
_column_125,
|
||||
_column_126,
|
||||
_column_127,
|
||||
_column_128,
|
||||
_column_129,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 stackEntity = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_130,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape36 localAssetEntity = Shape36(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape48 remoteAlbumEntity = Shape48(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_138,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_139,
|
||||
_column_140,
|
||||
_column_141,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 localAlbumEntity = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_115,
|
||||
_column_142,
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_146, _column_147, _column_145],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
||||
'idx_remote_asset_local_date_time_day',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
||||
'idx_remote_asset_local_date_time_month',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
||||
);
|
||||
late final Shape40 authUserEntity = Shape40(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_148,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_149,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_153, _column_154, _column_155],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape41 partnerEntity = Shape41(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_156, _column_157, _column_158],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape42 remoteExifEntity = Shape42(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_117,
|
||||
_column_116,
|
||||
_column_165,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_169,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_176,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_159, _column_177],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_177, _column_153, _column_178],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape44 memoryEntity = Shape44(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_124,
|
||||
_column_121,
|
||||
_column_113,
|
||||
_column_181,
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_159, _column_187],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape45 personEntity = Shape45(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_108,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_192,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape46 assetFaceEntity = Shape46(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_193,
|
||||
_column_194,
|
||||
_column_195,
|
||||
_column_196,
|
||||
_column_197,
|
||||
_column_198,
|
||||
_column_199,
|
||||
_column_200,
|
||||
_column_201,
|
||||
_column_124,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_202, _column_203, _column_204],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_205,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_206,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 assetEditEntity = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_edit_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_207,
|
||||
_column_208,
|
||||
_column_209,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape48 extends i0.VersionedTable {
|
||||
Shape48({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get description =>
|
||||
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get thumbnailAssetId =>
|
||||
columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isActivityEnabled =>
|
||||
columnsByName['is_activity_enabled']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get order =>
|
||||
columnsByName['order']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@ -11847,6 +12398,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
|
||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@ -11960,6 +12512,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from22To23(migrator, schema);
|
||||
return 23;
|
||||
case 23:
|
||||
final schema = Schema24(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from23To24(migrator, schema);
|
||||
return 24;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@ -11989,6 +12546,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
|
||||
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
|
||||
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@ -12013,5 +12571,6 @@ i1.OnUpgrade stepByStep({
|
||||
from20To21: from20To21,
|
||||
from21To22: from21To22,
|
||||
from22To23: from22To23,
|
||||
from23To24: from23To24,
|
||||
),
|
||||
);
|
||||
|
||||
@ -32,17 +32,23 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(_db.userEntity, _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), useColumns: false),
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
useColumns: false,
|
||||
),
|
||||
]);
|
||||
query
|
||||
..where(_db.remoteAssetEntity.deletedAt.isNull())
|
||||
..addColumns([assetCount])
|
||||
..addColumns([_db.userEntity.name])
|
||||
..addColumns([_db.userEntity.name, _db.userEntity.id])
|
||||
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||
..groupBy([_db.remoteAlbumEntity.id]);
|
||||
|
||||
@ -63,6 +69,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(
|
||||
assetCount: row.read(assetCount) ?? 0,
|
||||
ownerId: row.read(_db.userEntity.id)!,
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
),
|
||||
@ -85,20 +92,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAlbumEntity.id.equals(albumId) & _db.remoteAssetEntity.deletedAt.isNull())
|
||||
..addColumns([assetCount])
|
||||
..addColumns([_db.userEntity.name])
|
||||
..addColumns([_db.userEntity.name, _db.userEntity.id])
|
||||
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||
..groupBy([_db.remoteAlbumEntity.id]);
|
||||
|
||||
@ -108,6 +117,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(
|
||||
assetCount: row.read(assetCount) ?? 0,
|
||||
ownerId: row.read(_db.userEntity.id)!,
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
),
|
||||
@ -116,12 +126,29 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||
final query = _db.remoteAlbumEntity.select()
|
||||
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||
final query = _db.remoteAlbumEntity.select().join([
|
||||
innerJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
|
||||
_db.remoteAlbumUserEntity.userId.equals(ownerId) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
useColumns: false,
|
||||
),
|
||||
]);
|
||||
|
||||
query
|
||||
..addColumns([_db.remoteAlbumUserEntity.userId])
|
||||
..where(_db.remoteAlbumEntity.name.equals(albumName))
|
||||
..orderBy([OrderingTerm.desc(_db.remoteAlbumEntity.createdAt)])
|
||||
..limit(1);
|
||||
|
||||
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
|
||||
return query
|
||||
.map(
|
||||
(row) => row
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(ownerId: row.read(_db.remoteAlbumUserEntity.userId)!, ownerName: '', isShared: false),
|
||||
)
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
||||
@ -129,7 +156,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
final entity = RemoteAlbumEntityCompanion(
|
||||
id: Value(album.id),
|
||||
name: Value(album.name),
|
||||
ownerId: Value(album.ownerId),
|
||||
createdAt: Value(album.createdAt),
|
||||
updatedAt: Value(album.updatedAt),
|
||||
description: Value(album.description),
|
||||
@ -140,6 +166,14 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
|
||||
await _db.remoteAlbumEntity.insertOne(entity);
|
||||
|
||||
await _db.remoteAlbumUserEntity.insertOne(
|
||||
RemoteAlbumUserEntityCompanion(
|
||||
albumId: Value(album.id),
|
||||
userId: Value(album.ownerId),
|
||||
role: const Value(AlbumUserRole.owner),
|
||||
),
|
||||
);
|
||||
|
||||
if (assetIds.isNotEmpty) {
|
||||
final albumAssets = assetIds.map(
|
||||
(assetId) => RemoteAlbumAssetEntityCompanion(albumId: Value(album.id), assetId: Value(assetId)),
|
||||
@ -157,7 +191,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
RemoteAlbumEntityCompanion(
|
||||
id: Value(album.id),
|
||||
name: Value(album.name),
|
||||
ownerId: Value(album.ownerId),
|
||||
createdAt: Value(album.createdAt),
|
||||
updatedAt: Value(album.updatedAt),
|
||||
description: Value(album.description),
|
||||
@ -197,7 +230,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
Future<List<UserDto>> getSharedUsers(String albumId) async {
|
||||
final albumUserRows = await (_db.select(
|
||||
_db.remoteAlbumUserEntity,
|
||||
)..where((row) => row.albumId.equals(albumId))).get();
|
||||
)..where((row) => row.albumId.equals(albumId) & row.role.isNotValue(AlbumUserRole.owner.index))).get();
|
||||
|
||||
if (albumUserRows.isEmpty) {
|
||||
return [];
|
||||
@ -295,19 +328,21 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAlbumEntity.id.equals(albumId))
|
||||
..addColumns([_db.userEntity.name])
|
||||
..addColumns([_db.userEntity.name, _db.userEntity.id])
|
||||
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||
..groupBy([_db.remoteAlbumEntity.id]);
|
||||
|
||||
@ -315,6 +350,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
final album = row
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(
|
||||
ownerId: row.read(_db.userEntity.id)!,
|
||||
ownerName: row.read(_db.userEntity.name)!,
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
);
|
||||
@ -355,6 +391,37 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
return _db.managers.remoteAlbumEntity.count();
|
||||
}
|
||||
|
||||
Future<UserDto> getOwner(String albumId) {
|
||||
final query =
|
||||
_db.userEntity.select().join([
|
||||
innerJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId),
|
||||
useColumns: false,
|
||||
),
|
||||
])..where(
|
||||
_db.remoteAlbumUserEntity.albumId.equals(albumId) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
);
|
||||
|
||||
return query
|
||||
.map(
|
||||
(row) => UserDto(
|
||||
id: row.read(_db.userEntity.id)!,
|
||||
email: row.read(_db.userEntity.email)!,
|
||||
name: row.read(_db.userEntity.name)!,
|
||||
memoryEnabled: true,
|
||||
inTimeline: false,
|
||||
isPartnerSharedBy: false,
|
||||
isPartnerSharedWith: false,
|
||||
profileChangedAt: row.read(_db.userEntity.profileChangedAt)!,
|
||||
hasProfileImage: row.read(_db.userEntity.hasProfileImage)!,
|
||||
avatarColor: AvatarColor.values[row.read(_db.userEntity.avatarColor)!],
|
||||
),
|
||||
)
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
|
||||
// Find remote asset ids that:
|
||||
// 1. Belong to the provided local album (via local_album_asset_entity)
|
||||
@ -416,21 +483,23 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
|
||||
useColumns: false,
|
||||
),
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumUserEntity,
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.userEntity,
|
||||
_db.userEntity.id.equalsExp(_db.remoteAlbumUserEntity.userId) &
|
||||
_db.remoteAlbumUserEntity.albumId.equalsExp(_db.remoteAlbumEntity.id) &
|
||||
_db.remoteAlbumUserEntity.role.equalsValue(AlbumUserRole.owner),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.remoteAlbumEntity.id.isIn(albumIds) & _db.remoteAssetEntity.deletedAt.isNull())
|
||||
..addColumns([assetCount])
|
||||
..addColumns([_db.remoteAlbumUserEntity.userId.count(distinct: true)])
|
||||
..addColumns([_db.userEntity.name])
|
||||
..addColumns([_db.userEntity.name, _db.userEntity.id])
|
||||
..groupBy([_db.remoteAlbumEntity.id]);
|
||||
|
||||
return query
|
||||
@ -438,6 +507,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
(row) => row
|
||||
.readTable(_db.remoteAlbumEntity)
|
||||
.toDto(
|
||||
ownerId: row.read(_db.userEntity.id)!,
|
||||
ownerName: row.read(_db.userEntity.name) ?? '',
|
||||
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0,
|
||||
assetCount: row.read(assetCount) ?? 0,
|
||||
@ -448,7 +518,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
extension on RemoteAlbumEntityData {
|
||||
RemoteAlbum toDto({int assetCount = 0, required String ownerName, required bool isShared}) {
|
||||
RemoteAlbum toDto({int assetCount = 0, required String ownerName, required String ownerId, required bool isShared}) {
|
||||
return RemoteAlbum(
|
||||
id: id,
|
||||
name: name,
|
||||
|
||||
@ -53,7 +53,10 @@ class SyncApiRepository {
|
||||
SyncRequestType.partnersV1,
|
||||
SyncRequestType.partnerAssetsV1,
|
||||
SyncRequestType.partnerAssetExifsV1,
|
||||
SyncRequestType.albumsV1,
|
||||
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
|
||||
SyncRequestType.albumsV1
|
||||
else
|
||||
SyncRequestType.albumsV2,
|
||||
SyncRequestType.albumUsersV1,
|
||||
SyncRequestType.albumAssetsV1,
|
||||
SyncRequestType.albumAssetExifsV1,
|
||||
@ -162,6 +165,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.albumV1: SyncAlbumV1.fromJson,
|
||||
SyncEntityType.albumV2: SyncAlbumV2.fromJson,
|
||||
SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson,
|
||||
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
||||
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||
|
||||
@ -30,7 +30,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
|
||||
import 'package:openapi/api.dart' hide AlbumUserRole, UserMetadataKey, AssetEditAction, AssetVisibility;
|
||||
import 'package:openapi/api.dart' hide UserMetadataKey, AssetEditAction, AssetVisibility, AlbumUserRole;
|
||||
|
||||
class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
final Logger _logger = Logger('DriftSyncStreamRepository');
|
||||
@ -397,6 +397,47 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<void> updateAlbumsV1(Iterable<SyncAlbumV1> data) async {
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
await _db.batch((batch) {
|
||||
for (final album in data) {
|
||||
final companion = RemoteAlbumEntityCompanion(
|
||||
name: Value(album.name),
|
||||
description: Value(album.description),
|
||||
isActivityEnabled: Value(album.isActivityEnabled),
|
||||
order: Value(album.order.toAlbumAssetOrder()),
|
||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||
createdAt: Value(album.createdAt),
|
||||
updatedAt: Value(album.updatedAt),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.remoteAlbumEntity,
|
||||
companion.copyWith(id: Value(album.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await _db.batch((batch) {
|
||||
for (final album in data) {
|
||||
final companion = RemoteAlbumUserEntityCompanion(
|
||||
albumId: Value(album.id),
|
||||
userId: Value(album.ownerId),
|
||||
role: const Value(AlbumUserRole.owner),
|
||||
);
|
||||
|
||||
batch.insert(_db.remoteAlbumUserEntity, companion, onConflict: DoUpdate((_) => companion));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAlbumsV1', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAlbumsV2(Iterable<SyncAlbumV2> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final album in data) {
|
||||
@ -406,7 +447,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
isActivityEnabled: Value(album.isActivityEnabled),
|
||||
order: Value(album.order.toAlbumAssetOrder()),
|
||||
thumbnailAssetId: Value(album.thumbnailAssetId),
|
||||
ownerId: Value(album.ownerId),
|
||||
createdAt: Value(album.createdAt),
|
||||
updatedAt: Value(album.updatedAt),
|
||||
);
|
||||
@ -419,7 +459,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAlbumsV1', error, stack);
|
||||
_logger.severe('Error: updateAlbumsV2', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -821,6 +861,7 @@ extension on api.AlbumUserRole {
|
||||
AlbumUserRole toAlbumUserRole() => switch (this) {
|
||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||
api.AlbumUserRole.viewer => AlbumUserRole.viewer,
|
||||
api.AlbumUserRole.owner => AlbumUserRole.owner,
|
||||
_ => throw Exception('Unknown AlbumUserRole value: $this'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class RemoteAlbumState {
|
||||
@ -81,7 +82,17 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
List<String> assetIds = const [],
|
||||
}) async {
|
||||
try {
|
||||
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
throw Exception('User not logged in');
|
||||
}
|
||||
|
||||
final album = await _remoteAlbumService.createAlbum(
|
||||
title: title,
|
||||
owner: currentUser,
|
||||
description: description,
|
||||
assetIds: assetIds,
|
||||
);
|
||||
|
||||
state = state.copyWith(albums: [...state.albums, album]);
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
// ignore: import_rule_openapi
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:openapi/api.dart' hide AlbumUserRole;
|
||||
|
||||
final driftAlbumApiRepositoryProvider = Provider(
|
||||
(ref) => DriftAlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||
@ -14,12 +15,17 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
|
||||
DriftAlbumApiRepository(this._api);
|
||||
|
||||
Future<RemoteAlbum> createDriftAlbum(String name, {required Iterable<String> assetIds, String? description}) async {
|
||||
Future<RemoteAlbum> createDriftAlbum(
|
||||
String name,
|
||||
UserDto owner, {
|
||||
required Iterable<String> assetIds,
|
||||
String? description,
|
||||
}) async {
|
||||
final responseDto = await checkNull(
|
||||
_api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())),
|
||||
);
|
||||
|
||||
return responseDto.toRemoteAlbum();
|
||||
return responseDto.toRemoteAlbum(owner);
|
||||
}
|
||||
|
||||
Future<({List<String> removed, List<String> failed})> removeAssets(String albumId, Iterable<String> assetIds) async {
|
||||
@ -50,7 +56,8 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
String albumId, {
|
||||
String albumId,
|
||||
UserDto owner, {
|
||||
String? name,
|
||||
String? description,
|
||||
String? thumbnailAssetId,
|
||||
@ -75,17 +82,16 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
),
|
||||
);
|
||||
|
||||
return responseDto.toRemoteAlbum();
|
||||
return responseDto.toRemoteAlbum(owner);
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(String albumId) {
|
||||
return _api.deleteAlbum(albumId);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> addUsers(String albumId, Iterable<String> userIds) async {
|
||||
Future<void> addUsers(String albumId, Iterable<String> userIds) async {
|
||||
final albumUsers = userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
||||
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
||||
return response.toRemoteAlbum();
|
||||
await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
||||
}
|
||||
|
||||
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||
@ -99,11 +105,12 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
}
|
||||
|
||||
extension on AlbumResponseDto {
|
||||
RemoteAlbum toRemoteAlbum() {
|
||||
RemoteAlbum toRemoteAlbum(final UserDto user) {
|
||||
return RemoteAlbum(
|
||||
id: id,
|
||||
name: albumName,
|
||||
ownerId: owner.id,
|
||||
ownerId: user.id,
|
||||
ownerName: user.name,
|
||||
description: description,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
@ -111,7 +118,6 @@ extension on AlbumResponseDto {
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
|
||||
assetCount: assetCount,
|
||||
ownerName: owner.name,
|
||||
isShared: albumUsers.length > 2,
|
||||
);
|
||||
}
|
||||
|
||||
@ -241,7 +241,8 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<bool> setAlbumCover(String albumId, String assetId) async {
|
||||
final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, thumbnailAssetId: assetId);
|
||||
final owner = await _remoteAlbumRepository.getOwner(albumId);
|
||||
final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, owner, thumbnailAssetId: assetId);
|
||||
await _remoteAlbumRepository.update(updatedAlbum);
|
||||
return true;
|
||||
}
|
||||
|
||||
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -568,6 +568,7 @@ Class | Method | HTTP request | Description
|
||||
- [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md)
|
||||
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
|
||||
- [SyncAlbumV1](doc//SyncAlbumV1.md)
|
||||
- [SyncAlbumV2](doc//SyncAlbumV2.md)
|
||||
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
|
||||
- [SyncAssetEditDeleteV1](doc//SyncAssetEditDeleteV1.md)
|
||||
- [SyncAssetEditV1](doc//SyncAssetEditV1.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -316,6 +316,7 @@ part 'model/sync_album_to_asset_v1.dart';
|
||||
part 'model/sync_album_user_delete_v1.dart';
|
||||
part 'model/sync_album_user_v1.dart';
|
||||
part 'model/sync_album_v1.dart';
|
||||
part 'model/sync_album_v2.dart';
|
||||
part 'model/sync_asset_delete_v1.dart';
|
||||
part 'model/sync_asset_edit_delete_v1.dart';
|
||||
part 'model/sync_asset_edit_v1.dart';
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -678,6 +678,8 @@ class ApiClient {
|
||||
return SyncAlbumUserV1.fromJson(value);
|
||||
case 'SyncAlbumV1':
|
||||
return SyncAlbumV1.fromJson(value);
|
||||
case 'SyncAlbumV2':
|
||||
return SyncAlbumV2.fromJson(value);
|
||||
case 'SyncAssetDeleteV1':
|
||||
return SyncAssetDeleteV1.fromJson(value);
|
||||
case 'SyncAssetEditDeleteV1':
|
||||
|
||||
20
mobile/openapi/lib/model/album_response_dto.dart
generated
20
mobile/openapi/lib/model/album_response_dto.dart
generated
@ -26,8 +26,6 @@ class AlbumResponseDto {
|
||||
required this.isActivityEnabled,
|
||||
this.lastModifiedAssetTimestamp,
|
||||
this.order,
|
||||
required this.owner,
|
||||
required this.ownerId,
|
||||
required this.shared,
|
||||
this.startDate,
|
||||
required this.updatedAt,
|
||||
@ -39,6 +37,7 @@ class AlbumResponseDto {
|
||||
/// Thumbnail asset ID
|
||||
String? albumThumbnailAssetId;
|
||||
|
||||
/// First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.
|
||||
List<AlbumUserResponseDto> albumUsers;
|
||||
|
||||
/// Number of assets
|
||||
@ -90,11 +89,6 @@ class AlbumResponseDto {
|
||||
///
|
||||
AssetOrder? order;
|
||||
|
||||
UserResponseDto owner;
|
||||
|
||||
/// Owner user ID
|
||||
String ownerId;
|
||||
|
||||
/// Is shared album
|
||||
bool shared;
|
||||
|
||||
@ -125,8 +119,6 @@ class AlbumResponseDto {
|
||||
other.isActivityEnabled == isActivityEnabled &&
|
||||
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
||||
other.order == order &&
|
||||
other.owner == owner &&
|
||||
other.ownerId == ownerId &&
|
||||
other.shared == shared &&
|
||||
other.startDate == startDate &&
|
||||
other.updatedAt == updatedAt;
|
||||
@ -147,14 +139,12 @@ class AlbumResponseDto {
|
||||
(isActivityEnabled.hashCode) +
|
||||
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
||||
(order == null ? 0 : order!.hashCode) +
|
||||
(owner.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(shared.hashCode) +
|
||||
(startDate == null ? 0 : startDate!.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]';
|
||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -187,8 +177,6 @@ class AlbumResponseDto {
|
||||
} else {
|
||||
// json[r'order'] = null;
|
||||
}
|
||||
json[r'owner'] = this.owner;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'shared'] = this.shared;
|
||||
if (this.startDate != null) {
|
||||
json[r'startDate'] = this.startDate!.toUtc().toIso8601String();
|
||||
@ -221,8 +209,6 @@ class AlbumResponseDto {
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
|
||||
order: AssetOrder.fromJson(json[r'order']),
|
||||
owner: UserResponseDto.fromJson(json[r'owner'])!,
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
shared: mapValueOfType<bool>(json, r'shared')!,
|
||||
startDate: mapDateTime(json, r'startDate', r''),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
@ -282,8 +268,6 @@ class AlbumResponseDto {
|
||||
'hasSharedLink',
|
||||
'id',
|
||||
'isActivityEnabled',
|
||||
'owner',
|
||||
'ownerId',
|
||||
'shared',
|
||||
'updatedAt',
|
||||
};
|
||||
|
||||
3
mobile/openapi/lib/model/album_user_role.dart
generated
3
mobile/openapi/lib/model/album_user_role.dart
generated
@ -24,11 +24,13 @@ class AlbumUserRole {
|
||||
String toJson() => value;
|
||||
|
||||
static const editor = AlbumUserRole._(r'editor');
|
||||
static const owner = AlbumUserRole._(r'owner');
|
||||
static const viewer = AlbumUserRole._(r'viewer');
|
||||
|
||||
/// List of all possible values in this [enum][AlbumUserRole].
|
||||
static const values = <AlbumUserRole>[
|
||||
editor,
|
||||
owner,
|
||||
viewer,
|
||||
];
|
||||
|
||||
@ -69,6 +71,7 @@ class AlbumUserRoleTypeTransformer {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'editor': return AlbumUserRole.editor;
|
||||
case r'owner': return AlbumUserRole.owner;
|
||||
case r'viewer': return AlbumUserRole.viewer;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
|
||||
170
mobile/openapi/lib/model/sync_album_v2.dart
generated
Normal file
170
mobile/openapi/lib/model/sync_album_v2.dart
generated
Normal file
@ -0,0 +1,170 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAlbumV2 {
|
||||
/// Returns a new [SyncAlbumV2] instance.
|
||||
SyncAlbumV2({
|
||||
required this.createdAt,
|
||||
required this.description,
|
||||
required this.id,
|
||||
required this.isActivityEnabled,
|
||||
required this.name,
|
||||
required this.order,
|
||||
required this.thumbnailAssetId,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Created at
|
||||
DateTime createdAt;
|
||||
|
||||
/// Album description
|
||||
String description;
|
||||
|
||||
/// Album ID
|
||||
String id;
|
||||
|
||||
/// Is activity enabled
|
||||
bool isActivityEnabled;
|
||||
|
||||
/// Album name
|
||||
String name;
|
||||
|
||||
AssetOrder order;
|
||||
|
||||
/// Thumbnail asset ID
|
||||
String? thumbnailAssetId;
|
||||
|
||||
/// Updated at
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumV2 &&
|
||||
other.createdAt == createdAt &&
|
||||
other.description == description &&
|
||||
other.id == id &&
|
||||
other.isActivityEnabled == isActivityEnabled &&
|
||||
other.name == name &&
|
||||
other.order == order &&
|
||||
other.thumbnailAssetId == thumbnailAssetId &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isActivityEnabled.hashCode) +
|
||||
(name.hashCode) +
|
||||
(order.hashCode) +
|
||||
(thumbnailAssetId == null ? 0 : thumbnailAssetId!.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAlbumV2[createdAt=$createdAt, description=$description, id=$id, isActivityEnabled=$isActivityEnabled, name=$name, order=$order, thumbnailAssetId=$thumbnailAssetId, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.createdAt.millisecondsSinceEpoch
|
||||
: this.createdAt.toUtc().toIso8601String();
|
||||
json[r'description'] = this.description;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||
json[r'name'] = this.name;
|
||||
json[r'order'] = this.order;
|
||||
if (this.thumbnailAssetId != null) {
|
||||
json[r'thumbnailAssetId'] = this.thumbnailAssetId;
|
||||
} else {
|
||||
// json[r'thumbnailAssetId'] = null;
|
||||
}
|
||||
json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.updatedAt.millisecondsSinceEpoch
|
||||
: this.updatedAt.toUtc().toIso8601String();
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAlbumV2] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAlbumV2? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAlbumV2");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAlbumV2(
|
||||
createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
order: AssetOrder.fromJson(json[r'order'])!,
|
||||
thumbnailAssetId: mapValueOfType<String>(json, r'thumbnailAssetId'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAlbumV2> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumV2>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumV2.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAlbumV2> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAlbumV2>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAlbumV2.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAlbumV2-objects as value to a dart map
|
||||
static Map<String, List<SyncAlbumV2>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAlbumV2>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAlbumV2.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'createdAt',
|
||||
'description',
|
||||
'id',
|
||||
'isActivityEnabled',
|
||||
'name',
|
||||
'order',
|
||||
'thumbnailAssetId',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
||||
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
@ -44,6 +44,7 @@ class SyncEntityType {
|
||||
static const partnerStackDeleteV1 = SyncEntityType._(r'PartnerStackDeleteV1');
|
||||
static const partnerStackV1 = SyncEntityType._(r'PartnerStackV1');
|
||||
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
||||
static const albumV2 = SyncEntityType._(r'AlbumV2');
|
||||
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||
@ -97,6 +98,7 @@ class SyncEntityType {
|
||||
partnerStackDeleteV1,
|
||||
partnerStackV1,
|
||||
albumV1,
|
||||
albumV2,
|
||||
albumDeleteV1,
|
||||
albumUserV1,
|
||||
albumUserBackfillV1,
|
||||
@ -185,6 +187,7 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'PartnerStackDeleteV1': return SyncEntityType.partnerStackDeleteV1;
|
||||
case r'PartnerStackV1': return SyncEntityType.partnerStackV1;
|
||||
case r'AlbumV1': return SyncEntityType.albumV1;
|
||||
case r'AlbumV2': return SyncEntityType.albumV2;
|
||||
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||
|
||||
3
mobile/openapi/lib/model/sync_request_type.dart
generated
3
mobile/openapi/lib/model/sync_request_type.dart
generated
@ -24,6 +24,7 @@ class SyncRequestType {
|
||||
String toJson() => value;
|
||||
|
||||
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
||||
static const albumsV2 = SyncRequestType._(r'AlbumsV2');
|
||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||
@ -49,6 +50,7 @@ class SyncRequestType {
|
||||
/// List of all possible values in this [enum][SyncRequestType].
|
||||
static const values = <SyncRequestType>[
|
||||
albumsV1,
|
||||
albumsV2,
|
||||
albumUsersV1,
|
||||
albumToAssetsV1,
|
||||
albumAssetsV1,
|
||||
@ -109,6 +111,7 @@ class SyncRequestTypeTypeTransformer {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
||||
case r'AlbumsV2': return SyncRequestType.albumsV2;
|
||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||
|
||||
4
mobile/test/drift/main/generated/schema.dart
generated
4
mobile/test/drift/main/generated/schema.dart
generated
@ -27,6 +27,7 @@ import 'schema_v20.dart' as v20;
|
||||
import 'schema_v21.dart' as v21;
|
||||
import 'schema_v22.dart' as v22;
|
||||
import 'schema_v23.dart' as v23;
|
||||
import 'schema_v24.dart' as v24;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@ -78,6 +79,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v22.DatabaseAtV22(db);
|
||||
case 23:
|
||||
return v23.DatabaseAtV23(db);
|
||||
case 24:
|
||||
return v24.DatabaseAtV24(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@ -107,5 +110,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
];
|
||||
}
|
||||
|
||||
9131
mobile/test/drift/main/generated/schema_v24.dart
generated
Normal file
9131
mobile/test/drift/main/generated/schema_v24.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.d
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
@ -151,13 +152,13 @@ class MediumRepositoryContext {
|
||||
String? thumbnailAssetId,
|
||||
}) async {
|
||||
id = TestUtils.uuid(id);
|
||||
return db
|
||||
|
||||
final album = await db
|
||||
.into(db.remoteAlbumEntity)
|
||||
.insertReturning(
|
||||
RemoteAlbumEntityCompanion(
|
||||
id: Value(id),
|
||||
name: Value(name ?? 'remote_album_$id'),
|
||||
ownerId: Value(TestUtils.uuid(ownerId)),
|
||||
createdAt: Value(TestUtils.date(createdAt)),
|
||||
updatedAt: Value(TestUtils.date(updatedAt)),
|
||||
description: Value(description ?? 'Description for album $id'),
|
||||
@ -166,6 +167,18 @@ class MediumRepositoryContext {
|
||||
thumbnailAssetId: Value(thumbnailAssetId),
|
||||
),
|
||||
);
|
||||
|
||||
await db
|
||||
.into(db.remoteAlbumUserEntity)
|
||||
.insert(
|
||||
RemoteAlbumUserEntityCompanion.insert(
|
||||
albumId: id,
|
||||
userId: ownerId ?? const Uuid().v4(),
|
||||
role: AlbumUserRole.owner,
|
||||
),
|
||||
);
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
Future<void> insertRemoteAlbumAsset({required String albumId, required String assetId}) {
|
||||
|
||||
@ -15279,9 +15279,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"albumUsers": {
|
||||
"description": "First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AlbumUserResponseDto"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
},
|
||||
"assetCount": {
|
||||
@ -15330,13 +15332,6 @@
|
||||
"order": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
},
|
||||
"ownerId": {
|
||||
"description": "Owner user ID",
|
||||
"type": "string"
|
||||
},
|
||||
"shared": {
|
||||
"description": "Is shared album",
|
||||
"type": "boolean"
|
||||
@ -15362,8 +15357,6 @@
|
||||
"hasSharedLink",
|
||||
"id",
|
||||
"isActivityEnabled",
|
||||
"owner",
|
||||
"ownerId",
|
||||
"shared",
|
||||
"updatedAt"
|
||||
],
|
||||
@ -15453,6 +15446,7 @@
|
||||
"description": "Album user role",
|
||||
"enum": [
|
||||
"editor",
|
||||
"owner",
|
||||
"viewer"
|
||||
],
|
||||
"type": "string"
|
||||
@ -22531,6 +22525,59 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAlbumV2": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "Created at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Album description",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Album ID",
|
||||
"type": "string"
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"description": "Is activity enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "Album name",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
},
|
||||
"thumbnailAssetId": {
|
||||
"description": "Thumbnail asset ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Updated at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"description",
|
||||
"id",
|
||||
"isActivityEnabled",
|
||||
"name",
|
||||
"order",
|
||||
"thumbnailAssetId",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetDeleteV1": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
@ -23216,6 +23263,7 @@
|
||||
"PartnerStackDeleteV1",
|
||||
"PartnerStackV1",
|
||||
"AlbumV1",
|
||||
"AlbumV2",
|
||||
"AlbumDeleteV1",
|
||||
"AlbumUserV1",
|
||||
"AlbumUserBackfillV1",
|
||||
@ -23510,6 +23558,7 @@
|
||||
"description": "Sync request type",
|
||||
"enum": [
|
||||
"AlbumsV1",
|
||||
"AlbumsV2",
|
||||
"AlbumUsersV1",
|
||||
"AlbumToAssetsV1",
|
||||
"AlbumAssetsV1",
|
||||
|
||||
@ -457,6 +457,7 @@ export type AlbumResponseDto = {
|
||||
albumName: string;
|
||||
/** Thumbnail asset ID */
|
||||
albumThumbnailAssetId: string | null;
|
||||
/** First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically. */
|
||||
albumUsers: AlbumUserResponseDto[];
|
||||
/** Number of assets */
|
||||
assetCount: number;
|
||||
@ -476,9 +477,6 @@ export type AlbumResponseDto = {
|
||||
/** Last modified asset timestamp */
|
||||
lastModifiedAssetTimestamp?: string;
|
||||
order?: AssetOrder;
|
||||
owner: UserResponseDto;
|
||||
/** Owner user ID */
|
||||
ownerId: string;
|
||||
/** Is shared album */
|
||||
shared: boolean;
|
||||
/** Start date (earliest asset) */
|
||||
@ -2881,6 +2879,23 @@ export type SyncAlbumV1 = {
|
||||
/** Updated at */
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SyncAlbumV2 = {
|
||||
/** Created at */
|
||||
createdAt: string;
|
||||
/** Album description */
|
||||
description: string;
|
||||
/** Album ID */
|
||||
id: string;
|
||||
/** Is activity enabled */
|
||||
isActivityEnabled: boolean;
|
||||
/** Album name */
|
||||
name: string;
|
||||
order: AssetOrder;
|
||||
/** Thumbnail asset ID */
|
||||
thumbnailAssetId: string | null;
|
||||
/** Updated at */
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SyncAssetDeleteV1 = {
|
||||
/** Asset ID */
|
||||
assetId: string;
|
||||
@ -6731,6 +6746,7 @@ export enum AssetVisibility {
|
||||
}
|
||||
export enum AlbumUserRole {
|
||||
Editor = "editor",
|
||||
Owner = "owner",
|
||||
Viewer = "viewer"
|
||||
}
|
||||
export enum BulkIdErrorReason {
|
||||
@ -7110,6 +7126,7 @@ export enum SyncEntityType {
|
||||
PartnerStackDeleteV1 = "PartnerStackDeleteV1",
|
||||
PartnerStackV1 = "PartnerStackV1",
|
||||
AlbumV1 = "AlbumV1",
|
||||
AlbumV2 = "AlbumV2",
|
||||
AlbumDeleteV1 = "AlbumDeleteV1",
|
||||
AlbumUserV1 = "AlbumUserV1",
|
||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||
@ -7142,6 +7159,7 @@ export enum SyncEntityType {
|
||||
}
|
||||
export enum SyncRequestType {
|
||||
AlbumsV1 = "AlbumsV1",
|
||||
AlbumsV2 = "AlbumsV2",
|
||||
AlbumUsersV1 = "AlbumUsersV1",
|
||||
AlbumToAssetsV1 = "AlbumToAssetsV1",
|
||||
AlbumAssetsV1 = "AlbumAssetsV1",
|
||||
|
||||
@ -195,7 +195,6 @@ export type SharedLink = {
|
||||
};
|
||||
|
||||
export type Album = Selectable<AlbumTable> & {
|
||||
owner: ShallowDehydrateObject<User>;
|
||||
assets: ShallowDehydrateObject<Selectable<AssetTable>>[];
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import _ from 'lodash';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||
import { AlbumUser, AuthSharedLink } from 'src/database';
|
||||
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
|
||||
@ -104,7 +103,6 @@ const ContributorCountResponseSchema = z
|
||||
export const AlbumResponseSchema = z
|
||||
.object({
|
||||
id: z.string().describe('Album ID'),
|
||||
ownerId: z.string().describe('Owner user ID'),
|
||||
albumName: z.string().describe('Album name'),
|
||||
description: z.string().describe('Album description'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
@ -113,9 +111,13 @@ export const AlbumResponseSchema = z
|
||||
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
|
||||
albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
|
||||
shared: z.boolean().describe('Is shared album'),
|
||||
albumUsers: z.array(AlbumUserResponseSchema),
|
||||
albumUsers: z
|
||||
.array(AlbumUserResponseSchema)
|
||||
.min(1)
|
||||
.describe(
|
||||
'First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.',
|
||||
),
|
||||
hasSharedLink: z.boolean().describe('Has shared link'),
|
||||
owner: UserResponseSchema,
|
||||
assetCount: z.int().min(0).describe('Number of assets'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
lastModifiedAssetTimestamp: z
|
||||
@ -155,8 +157,6 @@ export type MapAlbumDto = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
id: string;
|
||||
ownerId: string;
|
||||
owner: ShallowDehydrateObject<User>;
|
||||
isActivityEnabled: boolean;
|
||||
order: AssetOrder;
|
||||
};
|
||||
@ -174,12 +174,10 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
|
||||
}
|
||||
}
|
||||
|
||||
const albumUsersSorted = _.orderBy(albumUsers, ['role', 'user.name']);
|
||||
|
||||
const assets = entity.assets || [];
|
||||
|
||||
const hasSharedLink = !!entity.sharedLinks && entity.sharedLinks.length > 0;
|
||||
const hasSharedUser = albumUsers.length > 0;
|
||||
const hasSharedUser = albumUsers.length > 1;
|
||||
|
||||
let startDate = assets.at(0)?.localDateTime;
|
||||
let endDate = assets.at(-1)?.localDateTime;
|
||||
@ -195,9 +193,7 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
|
||||
createdAt: asDateString(entity.createdAt),
|
||||
updatedAt: asDateString(entity.updatedAt),
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
owner: mapUser(entity.owner),
|
||||
albumUsers: albumUsersSorted,
|
||||
albumUsers,
|
||||
shared: hasSharedUser || hasSharedLink,
|
||||
hasSharedLink,
|
||||
startDate: asDateString(startDate),
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { AssetEditActionSchema } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AlbumUserRoleSchema,
|
||||
AssetOrderSchema,
|
||||
AssetTypeSchema,
|
||||
@ -211,6 +212,19 @@ const SyncAlbumV1Schema = z
|
||||
})
|
||||
.meta({ id: 'SyncAlbumV1' });
|
||||
|
||||
const SyncAlbumV2Schema = z
|
||||
.object({
|
||||
id: z.string().describe('Album ID'),
|
||||
name: z.string().describe('Album name'),
|
||||
description: z.string().describe('Album description'),
|
||||
createdAt: isoDatetimeToDate.describe('Created at'),
|
||||
updatedAt: isoDatetimeToDate.describe('Updated at'),
|
||||
thumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
|
||||
isActivityEnabled: z.boolean().describe('Is activity enabled'),
|
||||
order: AssetOrderSchema,
|
||||
})
|
||||
.meta({ id: 'SyncAlbumV2' });
|
||||
|
||||
const SyncAlbumToAssetV1Schema = z
|
||||
.object({
|
||||
albumId: z.string().describe('Album ID'),
|
||||
@ -234,10 +248,21 @@ class SyncAlbumUserV1 extends createZodDto(SyncAlbumUserV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAlbumV1 extends createZodDto(SyncAlbumV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAlbumV2 extends createZodDto(SyncAlbumV2Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAlbumToAssetV1 extends createZodDto(SyncAlbumToAssetV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAlbumToAssetDeleteV1 extends createZodDto(SyncAlbumToAssetDeleteV1Schema) {}
|
||||
|
||||
export function syncAlbumV2ToV1(
|
||||
albumV2: SyncAlbumV2,
|
||||
albumUsers: { userId: string; role: AlbumUserRole }[],
|
||||
): SyncAlbumV1 {
|
||||
const owner = albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
|
||||
return { ...albumV2, ownerId: owner.userId };
|
||||
}
|
||||
|
||||
const SyncMemoryV1Schema = z
|
||||
.object({
|
||||
id: z.string().describe('Memory ID'),
|
||||
@ -407,6 +432,7 @@ export type SyncItem = {
|
||||
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.PartnerAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumV1]: SyncAlbumV1;
|
||||
[SyncEntityType.AlbumV2]: SyncAlbumV2;
|
||||
[SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1;
|
||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
|
||||
@ -61,6 +61,7 @@ export enum AssetFileType {
|
||||
|
||||
export enum AlbumUserRole {
|
||||
Editor = 'editor',
|
||||
Owner = 'owner',
|
||||
Viewer = 'viewer',
|
||||
}
|
||||
|
||||
@ -797,6 +798,7 @@ export enum ExitCode {
|
||||
|
||||
export enum SyncRequestType {
|
||||
AlbumsV1 = 'AlbumsV1',
|
||||
AlbumsV2 = 'AlbumsV2',
|
||||
AlbumUsersV1 = 'AlbumUsersV1',
|
||||
AlbumToAssetsV1 = 'AlbumToAssetsV1',
|
||||
AlbumAssetsV1 = 'AlbumAssetsV1',
|
||||
@ -852,6 +854,7 @@ export enum SyncEntityType {
|
||||
PartnerStackV1 = 'PartnerStackV1',
|
||||
|
||||
AlbumV1 = 'AlbumV1',
|
||||
AlbumV2 = 'AlbumV2',
|
||||
AlbumDeleteV1 = 'AlbumDeleteV1',
|
||||
|
||||
AlbumUserV1 = 'AlbumUserV1',
|
||||
|
||||
@ -14,27 +14,26 @@ select
|
||||
"activity"."id"
|
||||
from
|
||||
"activity"
|
||||
left join "album" on "activity"."albumId" = "album"."id"
|
||||
inner join "album" on "activity"."albumId" = "album"."id"
|
||||
and "album"."deletedAt" is null
|
||||
inner join "album_user" on "album"."id" = "album_user"."albumId"
|
||||
and "album_user"."role" = 'owner'
|
||||
and "album_user"."userId" = $1::uuid
|
||||
where
|
||||
"activity"."id" in ($1)
|
||||
and "album"."ownerId" = $2::uuid
|
||||
"activity"."id" in ($2)
|
||||
|
||||
-- AccessRepository.activity.checkCreateAccess
|
||||
select
|
||||
"album"."id"
|
||||
from
|
||||
"album"
|
||||
left join "album_user" as "albumUsers" on "albumUsers"."albumId" = "album"."id"
|
||||
left join "user" on "user"."id" = "albumUsers"."userId"
|
||||
inner join "album_user" as "albumUsers" on "albumUsers"."albumId" = "album"."id"
|
||||
inner join "user" on "user"."id" = "albumUsers"."userId"
|
||||
and "user"."deletedAt" is null
|
||||
where
|
||||
"album"."id" in ($1)
|
||||
and "album"."isActivityEnabled" = $2
|
||||
and (
|
||||
"album"."ownerId" = $3
|
||||
or "user"."id" = $4
|
||||
)
|
||||
and "user"."id" = $3
|
||||
and "album"."deletedAt" is null
|
||||
|
||||
-- AccessRepository.album.checkOwnerAccess
|
||||
@ -42,9 +41,11 @@ select
|
||||
"album"."id"
|
||||
from
|
||||
"album"
|
||||
inner join "album_user" on "album"."id" = "album_user"."albumId"
|
||||
and "album_user"."role" = 'owner'
|
||||
and "album_user"."userId" = $1
|
||||
where
|
||||
"album"."id" in ($1)
|
||||
and "album"."ownerId" = $2
|
||||
"album"."id" in ($2)
|
||||
and "album"."deletedAt" is null
|
||||
|
||||
-- AccessRepository.album.checkSharedAlbumAccess
|
||||
@ -52,8 +53,8 @@ select
|
||||
"album"."id"
|
||||
from
|
||||
"album"
|
||||
left join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
left join "user" on "user"."id" = "album_user"."userId"
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
and "user"."deletedAt" is null
|
||||
where
|
||||
"album"."id" in ($1)
|
||||
@ -93,10 +94,7 @@ where
|
||||
"asset"."id" = any (target.ids)
|
||||
or "asset"."livePhotoVideoId" = any (target.ids)
|
||||
)
|
||||
and (
|
||||
"album"."ownerId" = $2
|
||||
or "user"."id" = $3
|
||||
)
|
||||
and "user"."id" = $2
|
||||
and "album"."deletedAt" is null
|
||||
|
||||
-- AccessRepository.asset.checkOwnerAccess
|
||||
|
||||
@ -1,26 +1,17 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- AlbumRepository.getById
|
||||
with
|
||||
"album_user" as (
|
||||
select
|
||||
*
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = $1
|
||||
)
|
||||
select
|
||||
"album".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@ -41,15 +32,21 @@ select
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album_user"."userId"
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"album_user"."userId" = $2 desc,
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
@ -88,30 +85,12 @@ select
|
||||
from
|
||||
"album"
|
||||
where
|
||||
"album"."id" = $1
|
||||
"album"."id" = $3
|
||||
and "album"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
select
|
||||
"album".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@ -132,36 +111,38 @@ select
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album_user"."userId"
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"album_user"."userId" = $1 desc,
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers"
|
||||
from
|
||||
"album"
|
||||
inner join "album_asset" on "album_asset"."albumId" = "album"."id"
|
||||
where
|
||||
(
|
||||
"album"."ownerId" = $1
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $2
|
||||
)
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $2
|
||||
)
|
||||
and "album_asset"."assetId" = $3
|
||||
and "album"."deletedAt" is null
|
||||
order by
|
||||
"album"."createdAt" desc,
|
||||
"album"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getByAssetIds
|
||||
@ -172,18 +153,15 @@ from
|
||||
"album"
|
||||
inner join "album_asset" on "album_asset"."albumId" = "album"."id"
|
||||
where
|
||||
(
|
||||
"album"."ownerId" = $1
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $2
|
||||
)
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $1
|
||||
)
|
||||
and "album_asset"."assetId" in ($3)
|
||||
and "album_asset"."assetId" in ($2)
|
||||
and "album"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getMetadataForIds
|
||||
@ -210,24 +188,6 @@ group by
|
||||
-- AlbumRepository.getOwned
|
||||
select
|
||||
"album".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@ -248,15 +208,21 @@ select
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album_user"."userId"
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"album_user"."userId" = $1 desc,
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
@ -274,9 +240,11 @@ select
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"album"
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $2
|
||||
and "album_user"."role" = 'owner'
|
||||
where
|
||||
"album"."ownerId" = $1
|
||||
and "album"."deletedAt" is null
|
||||
"album"."deletedAt" is null
|
||||
order by
|
||||
"album"."createdAt" desc
|
||||
|
||||
@ -303,35 +271,23 @@ select
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album_user"."userId"
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"album_user"."userId" = $1 desc,
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@ -347,29 +303,34 @@ select
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"album"
|
||||
inner join (
|
||||
select
|
||||
"album_user"."albumId" as "id"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $2
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"album_user"."albumId"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."role" != 'owner'
|
||||
)
|
||||
union
|
||||
select
|
||||
"shared_link"."albumId" as "id"
|
||||
from
|
||||
"shared_link"
|
||||
where
|
||||
"shared_link"."userId" = $3
|
||||
and "shared_link"."albumId" is not null
|
||||
) as "matching" on "matching"."id" = "album"."id"
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."role" = 'owner'
|
||||
where
|
||||
(
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
and (
|
||||
"album"."ownerId" = $1
|
||||
or "album_user"."userId" = $2
|
||||
)
|
||||
)
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"shared_link"
|
||||
where
|
||||
"shared_link"."albumId" = "album"."id"
|
||||
and "shared_link"."userId" = $3
|
||||
)
|
||||
)
|
||||
and "album"."deletedAt" is null
|
||||
"album"."deletedAt" is null
|
||||
order by
|
||||
"album"."createdAt" desc
|
||||
|
||||
@ -378,33 +339,68 @@ select
|
||||
"album".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
"shared_link".*
|
||||
from
|
||||
"user"
|
||||
"shared_link"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
) as obj
|
||||
) as "owner"
|
||||
"shared_link"."albumId" = "album"."id"
|
||||
) as agg
|
||||
) as "sharedLinks",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_user"."role",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"album_user"."userId" = $1 desc,
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers"
|
||||
from
|
||||
"album"
|
||||
inner join "album_user" on "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = $2
|
||||
and "album_user"."role" = 'owner'
|
||||
where
|
||||
"album"."ownerId" = $1
|
||||
and "album"."deletedAt" is null
|
||||
"album"."deletedAt" is null
|
||||
and not exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
"album_user" as "au"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
"au"."albumId" = "album"."id"
|
||||
and "au"."role" != 'owner'
|
||||
)
|
||||
and not exists (
|
||||
select
|
||||
@ -430,6 +426,117 @@ where
|
||||
"album_asset"."albumId" = $1
|
||||
and "album_asset"."assetId" in ($2)
|
||||
|
||||
-- AlbumRepository.addAssetIds
|
||||
insert into
|
||||
"album_asset"
|
||||
select
|
||||
$1::uuid as "albumId",
|
||||
unnest($2::uuid[]) as "assetId"
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
on conflict do nothing
|
||||
|
||||
-- AlbumRepository.create
|
||||
with
|
||||
"album" as (
|
||||
insert into
|
||||
"album" ("albumName")
|
||||
values
|
||||
($1)
|
||||
returning
|
||||
*
|
||||
),
|
||||
"album_user" as (
|
||||
insert into
|
||||
"album_user"
|
||||
select
|
||||
"album"."id" as "albumId",
|
||||
unnest($2::uuid[]) as "userId",
|
||||
unnest($3::album_user_role_enum[]) as "role"
|
||||
from
|
||||
"album"
|
||||
returning
|
||||
"album_user"."albumId",
|
||||
"album_user"."userId",
|
||||
"album_user"."role"
|
||||
),
|
||||
"album_asset" as (
|
||||
insert into
|
||||
"album_asset"
|
||||
select
|
||||
"album"."id" as "albumId",
|
||||
unnest($4::uuid[]) as "assetId"
|
||||
from
|
||||
"album"
|
||||
on conflict do nothing
|
||||
returning
|
||||
"album_asset"."albumId",
|
||||
"album_asset"."assetId"
|
||||
)
|
||||
select
|
||||
"album".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_user"."role",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"album_user"
|
||||
inner join "user" on "user"."id" = "album_user"."userId"
|
||||
where
|
||||
"album_user"."albumId" = "album"."id"
|
||||
order by
|
||||
"album_user"."role",
|
||||
"user"."name" asc
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
json_agg("asset") as "assets"
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset".*,
|
||||
"asset_exif" as "exifInfo"
|
||||
from
|
||||
"asset"
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
inner join "album_asset" on "album_asset"."assetId" = "asset"."id"
|
||||
where
|
||||
"album_asset"."albumId" = "album"."id"
|
||||
and "asset"."deletedAt" is null
|
||||
and "asset"."visibility" in ('archive', 'timeline')
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc
|
||||
) as "asset"
|
||||
) as "assets"
|
||||
from
|
||||
"album"
|
||||
|
||||
-- AlbumRepository.getContributorCounts
|
||||
select
|
||||
"asset"."ownerId" as "userId",
|
||||
|
||||
@ -139,7 +139,15 @@ from
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."role" = 'owner'
|
||||
and "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = "user"."id"
|
||||
)
|
||||
and "user"."deletedAt" is null
|
||||
) as "owner" on true
|
||||
where
|
||||
@ -201,7 +209,15 @@ from
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."id" = "album"."ownerId"
|
||||
exists (
|
||||
select
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."role" = 'owner'
|
||||
and "album_user"."albumId" = "album"."id"
|
||||
and "album_user"."userId" = "user"."id"
|
||||
)
|
||||
and "user"."deletedAt" is null
|
||||
) as "owner" on true
|
||||
where
|
||||
|
||||
@ -29,7 +29,6 @@ order by
|
||||
-- SyncRepository.album.getUpserts
|
||||
select distinct
|
||||
on ("album"."id", "album"."updateId") "album"."id",
|
||||
"album"."ownerId",
|
||||
"album"."albumName" as "name",
|
||||
"album"."description",
|
||||
"album"."createdAt",
|
||||
@ -44,13 +43,19 @@ from
|
||||
where
|
||||
"album"."updateId" < $1
|
||||
and "album"."updateId" > $2
|
||||
and (
|
||||
"album"."ownerId" = $3
|
||||
or "album_users"."userId" = $4
|
||||
)
|
||||
and "album_users"."userId" = $3
|
||||
order by
|
||||
"album"."updateId" asc
|
||||
|
||||
-- SyncRepository.album.getAlbumUsers
|
||||
select
|
||||
"userId",
|
||||
"role"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"albumId" = $1
|
||||
|
||||
-- SyncRepository.albumAsset.getBackfill
|
||||
select
|
||||
"asset"."id",
|
||||
@ -109,16 +114,12 @@ select
|
||||
from
|
||||
"asset" as "asset"
|
||||
inner join "album_asset" on "album_asset"."assetId" = "asset"."id"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
left join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
where
|
||||
"asset"."updateId" < $1
|
||||
and "asset"."updateId" > $2
|
||||
and "album_asset"."updateId" <= $3
|
||||
and (
|
||||
"album"."ownerId" = $4
|
||||
or "album_user"."userId" = $5
|
||||
)
|
||||
and "album_user"."userId" = $4
|
||||
order by
|
||||
"asset"."updateId" asc
|
||||
|
||||
@ -147,15 +148,11 @@ select
|
||||
from
|
||||
"album_asset" as "album_asset"
|
||||
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
left join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
where
|
||||
"album_asset"."updateId" < $1
|
||||
and "album_asset"."updateId" > $2
|
||||
and (
|
||||
"album"."ownerId" = $3
|
||||
or "album_user"."userId" = $4
|
||||
)
|
||||
and "album_user"."userId" = $3
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
|
||||
@ -229,16 +226,12 @@ select
|
||||
from
|
||||
"asset_exif" as "asset_exif"
|
||||
inner join "album_asset" on "album_asset"."assetId" = "asset_exif"."assetId"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
left join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
where
|
||||
"asset_exif"."updateId" < $1
|
||||
and "asset_exif"."updateId" > $2
|
||||
and "album_asset"."updateId" <= $3
|
||||
and (
|
||||
"album"."ownerId" = $4
|
||||
or "album_user"."userId" = $5
|
||||
)
|
||||
and "album_user"."userId" = $4
|
||||
order by
|
||||
"asset_exif"."updateId" asc
|
||||
|
||||
@ -278,10 +271,7 @@ from
|
||||
where
|
||||
"album_asset"."updateId" < $1
|
||||
and "album_asset"."updateId" > $2
|
||||
and (
|
||||
"album"."ownerId" = $3
|
||||
or "album_user"."userId" = $4
|
||||
)
|
||||
and "album_user"."userId" = $3
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
|
||||
@ -312,20 +302,11 @@ where
|
||||
and "album_asset_audit"."id" > $2
|
||||
and "albumId" in (
|
||||
select
|
||||
"id"
|
||||
"album_user"."albumId" as "id"
|
||||
from
|
||||
"album"
|
||||
"album_user"
|
||||
where
|
||||
"ownerId" = $3
|
||||
union
|
||||
(
|
||||
select
|
||||
"album_user"."albumId" as "id"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $4
|
||||
)
|
||||
"album_user"."userId" = $3
|
||||
)
|
||||
order by
|
||||
"album_asset_audit"."id" asc
|
||||
@ -337,15 +318,11 @@ select
|
||||
"album_asset"."updateId"
|
||||
from
|
||||
"album_asset" as "album_asset"
|
||||
inner join "album" on "album"."id" = "album_asset"."albumId"
|
||||
left join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
|
||||
where
|
||||
"album_asset"."updateId" < $1
|
||||
and "album_asset"."updateId" > $2
|
||||
and (
|
||||
"album"."ownerId" = $3
|
||||
or "album_user"."userId" = $4
|
||||
)
|
||||
and "album_user"."userId" = $3
|
||||
order by
|
||||
"album_asset"."updateId" asc
|
||||
|
||||
@ -377,20 +354,11 @@ where
|
||||
and "album_user_audit"."id" > $2
|
||||
and "albumId" in (
|
||||
select
|
||||
"id"
|
||||
"album_user"."albumId" as "id"
|
||||
from
|
||||
"album"
|
||||
"album_user"
|
||||
where
|
||||
"ownerId" = $3
|
||||
union
|
||||
(
|
||||
select
|
||||
"album_user"."albumId" as "id"
|
||||
from
|
||||
"album_user"
|
||||
where
|
||||
"album_user"."userId" = $4
|
||||
)
|
||||
"album_user"."userId" = $3
|
||||
)
|
||||
order by
|
||||
"album_user_audit"."id" asc
|
||||
@ -408,20 +376,11 @@ where
|
||||
and "album_user"."updateId" > $2
|
||||
and "album_user"."albumId" in (
|
||||
select
|
||||
"id"
|
||||
"albumUsers"."albumId" as "id"
|
||||
from
|
||||
"album"
|
||||
"album_user" as "albumUsers"
|
||||
where
|
||||
"ownerId" = $3
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumId" as "id"
|
||||
from
|
||||
"album_user" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."userId" = $4
|
||||
)
|
||||
"albumUsers"."userId" = $3
|
||||
)
|
||||
order by
|
||||
"album_user"."updateId" asc
|
||||
|
||||
@ -35,9 +35,14 @@ class ActivityAccess {
|
||||
return this.db
|
||||
.selectFrom('activity')
|
||||
.select('activity.id')
|
||||
.leftJoin('album', (join) => join.onRef('activity.albumId', '=', 'album.id').on('album.deletedAt', 'is', null))
|
||||
.innerJoin('album', (join) => join.onRef('activity.albumId', '=', 'album.id').on('album.deletedAt', 'is', null))
|
||||
.innerJoin('album_user', (join) =>
|
||||
join
|
||||
.onRef('album.id', '=', 'album_user.albumId')
|
||||
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner))
|
||||
.on('album_user.userId', '=', asUuid(userId)),
|
||||
)
|
||||
.where('activity.id', 'in', [...activityIds])
|
||||
.whereRef('album.ownerId', '=', asUuid(userId))
|
||||
.execute()
|
||||
.then((activities) => new Set(activities.map((activity) => activity.id)));
|
||||
}
|
||||
@ -52,11 +57,11 @@ class ActivityAccess {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.select('album.id')
|
||||
.leftJoin('album_user as albumUsers', 'albumUsers.albumId', 'album.id')
|
||||
.leftJoin('user', (join) => join.onRef('user.id', '=', 'albumUsers.userId').on('user.deletedAt', 'is', null))
|
||||
.innerJoin('album_user as albumUsers', 'albumUsers.albumId', 'album.id')
|
||||
.innerJoin('user', (join) => join.onRef('user.id', '=', 'albumUsers.userId').on('user.deletedAt', 'is', null))
|
||||
.where('album.id', 'in', [...albumIds])
|
||||
.where('album.isActivityEnabled', '=', true)
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('user.id', '=', userId)]))
|
||||
.where((eb) => eb('user.id', '=', userId))
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.execute()
|
||||
.then((albums) => new Set(albums.map((album) => album.id)));
|
||||
@ -77,7 +82,12 @@ class AlbumAccess {
|
||||
.selectFrom('album')
|
||||
.select('album.id')
|
||||
.where('album.id', 'in', [...albumIds])
|
||||
.where('album.ownerId', '=', userId)
|
||||
.innerJoin('album_user', (join) =>
|
||||
join
|
||||
.onRef('album.id', '=', 'album_user.albumId')
|
||||
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner))
|
||||
.on('album_user.userId', '=', userId),
|
||||
)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.execute()
|
||||
.then((albums) => new Set(albums.map((album) => album.id)));
|
||||
@ -96,8 +106,8 @@ class AlbumAccess {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.select('album.id')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album.id')
|
||||
.leftJoin('user', (join) => join.onRef('user.id', '=', 'album_user.userId').on('user.deletedAt', 'is', null))
|
||||
.innerJoin('album_user', 'album_user.albumId', 'album.id')
|
||||
.innerJoin('user', (join) => join.onRef('user.id', '=', 'album_user.userId').on('user.deletedAt', 'is', null))
|
||||
.where('album.id', 'in', [...albumIds])
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.where('user.id', '=', userId)
|
||||
@ -152,7 +162,7 @@ class AssetAccess {
|
||||
eb('asset.livePhotoVideoId', '=', sql<string>`any(target.ids)`),
|
||||
]),
|
||||
)
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('user.id', '=', userId)]))
|
||||
.where('user.id', '=', userId)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.execute()
|
||||
.then((assets) => {
|
||||
|
||||
@ -7,7 +7,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
import { asUuid, dummy } from 'src/utils/database';
|
||||
|
||||
export interface ActivitySearch {
|
||||
albumId?: string;
|
||||
@ -31,11 +31,7 @@ export class ActivityRepository {
|
||||
join.onRef('user2.id', '=', 'activity.userId').on('user2.deletedAt', 'is', null),
|
||||
)
|
||||
.innerJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom(sql`(select 1)`.as('dummy'))
|
||||
.select(columns.userWithPrefix)
|
||||
.as('user'),
|
||||
(eb) => eb.selectFrom(dummy).select(columns.userWithPrefix).as('user'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
|
||||
@ -14,10 +14,11 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { withDefaultVisibility } from 'src/utils/database';
|
||||
import { asUuid, dummy, withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
@ -31,33 +32,25 @@ export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'album.ownerId'))
|
||||
.$notNull()
|
||||
.as('owner');
|
||||
};
|
||||
|
||||
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return jsonArrayFrom(
|
||||
const withAlbumUsers = (authUserId?: string) => (eb: ExpressionBuilder<DB, 'album'>) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.innerJoin('user', 'user.id', 'album_user.userId')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.select('album_user.role')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.selectFrom('user').select(columns.user).whereRef('user.id', '=', 'album_user.userId'))
|
||||
.$notNull()
|
||||
.as('user'),
|
||||
)
|
||||
.whereRef('album_user.albumId', '=', 'album.id'),
|
||||
.select((eb) => jsonObjectFrom(eb.selectFrom(dummy).select(columns.user)).$notNull().as('user'))
|
||||
.orderBy('album_user.role')
|
||||
.$if(!!authUserId, (qb) => qb.orderBy((eb) => eb('album_user.userId', '=', authUserId!), 'desc'))
|
||||
.orderBy('user.name', 'asc'),
|
||||
)
|
||||
.$notNull()
|
||||
.as('albumUsers');
|
||||
};
|
||||
|
||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return jsonArrayFrom(
|
||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'album'>) =>
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('shared_link').selectAll('shared_link').whereRef('shared_link.albumId', '=', 'album.id'),
|
||||
).as('sharedLinks');
|
||||
};
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return eb
|
||||
@ -80,19 +73,28 @@ const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
.as('assets');
|
||||
};
|
||||
|
||||
const isAlbumOwned = (ownerId: string) => (eb: ExpressionBuilder<DB, 'album'>) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where('album_user.role', '=', AlbumUserRole.Owner)
|
||||
.where('album_user.userId', '=', ownerId),
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class AlbumRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||
async getById(id: string, options: AlbumInfoOptions) {
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }, DummyValue.UUID] })
|
||||
getById(id: string, options: AlbumInfoOptions, authUserId?: string) {
|
||||
return this.db
|
||||
.with('album_user', (qb) => qb.selectFrom('album_user').selectAll().where('album_user.albumId', '=', id))
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where('album.id', '=', id)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withAlbumUsers(authUserId))
|
||||
.select(withSharedLink)
|
||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
@ -100,27 +102,22 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string) {
|
||||
getByAssetId(ownerId: string, assetId: string) {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.innerJoin('album_asset', 'album_asset.albumId', 'album.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('album.ownerId', '=', ownerId),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where('album_user.userId', '=', ownerId),
|
||||
),
|
||||
]),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where('album_user.userId', '=', ownerId),
|
||||
),
|
||||
)
|
||||
.where('album_asset.assetId', '=', assetId)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withAlbumUsers(ownerId))
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
@ -137,15 +134,12 @@ export class AlbumRepository {
|
||||
.select('album.id')
|
||||
.innerJoin('album_asset', 'album_asset.albumId', 'album.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('album.ownerId', '=', ownerId),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where('album_user.userId', '=', ownerId),
|
||||
),
|
||||
]),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where('album_user.userId', '=', ownerId),
|
||||
),
|
||||
)
|
||||
.where('album_asset.assetId', 'in', assetIds)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
@ -190,15 +184,19 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getOwned(ownerId: string) {
|
||||
getOwned(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.where('album.ownerId', '=', ownerId)
|
||||
.innerJoin('album_user', (join) =>
|
||||
join
|
||||
.onRef('album_user.albumId', '=', 'album.id')
|
||||
.on('album_user.userId', '=', ownerId)
|
||||
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner)),
|
||||
)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers(ownerId))
|
||||
.select(withSharedLink)
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
@ -207,29 +205,40 @@ export class AlbumRepository {
|
||||
* Get albums shared with and shared by owner.
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getShared(ownerId: string) {
|
||||
getShared(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', ownerId), eb('album_user.userId', '=', ownerId)])),
|
||||
),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('shared_link')
|
||||
.whereRef('shared_link.albumId', '=', 'album.id')
|
||||
.where('shared_link.userId', '=', ownerId),
|
||||
),
|
||||
]),
|
||||
.innerJoin(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.albumId as id')
|
||||
.where('album_user.userId', '=', ownerId)
|
||||
.where(
|
||||
'album_user.albumId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select('album_user.albumId')
|
||||
.where('album_user.role', '!=', sql.lit(AlbumUserRole.Owner)),
|
||||
)
|
||||
.union(
|
||||
eb
|
||||
.selectFrom('shared_link')
|
||||
.where('shared_link.userId', '=', ownerId)
|
||||
.where('shared_link.albumId', 'is not', null)
|
||||
.select('shared_link.albumId as id')
|
||||
.$narrowType<{ id: NotNull }>(),
|
||||
)
|
||||
.as('matching'),
|
||||
(join) => join.onRef('matching.id', '=', 'album.id'),
|
||||
)
|
||||
.innerJoin('album_user', (join) =>
|
||||
join.onRef('album_user.albumId', '=', 'album.id').on('album_user.role', '=', sql.lit(AlbumUserRole.Owner)),
|
||||
)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers(ownerId))
|
||||
.select(withSharedLink)
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.execute();
|
||||
@ -239,29 +248,45 @@ export class AlbumRepository {
|
||||
* Get albums of owner that are _not_ shared
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNotShared(ownerId: string) {
|
||||
getNotShared(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where('album.ownerId', '=', ownerId)
|
||||
.innerJoin('album_user', (join) =>
|
||||
join
|
||||
.onRef('album_user.albumId', '=', 'album.id')
|
||||
.on('album_user.userId', '=', ownerId)
|
||||
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner)),
|
||||
)
|
||||
.where('album.deletedAt', 'is', null)
|
||||
.where((eb) => eb.not(eb.exists(eb.selectFrom('album_user').whereRef('album_user.albumId', '=', 'album.id'))))
|
||||
.where((eb) => eb.not(eb.exists(eb.selectFrom('shared_link').whereRef('shared_link.albumId', '=', 'album.id'))))
|
||||
.select(withOwner)
|
||||
.where(({ not, exists, selectFrom }) =>
|
||||
not(
|
||||
exists(
|
||||
selectFrom('album_user as au')
|
||||
.whereRef('au.albumId', '=', 'album.id')
|
||||
.where('au.role', '!=', sql.lit(AlbumUserRole.Owner)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where(({ not, exists, selectFrom }) =>
|
||||
not(exists(selectFrom('shared_link').whereRef('shared_link.albumId', '=', 'album.id'))),
|
||||
)
|
||||
.select(withSharedLink)
|
||||
.select(withAlbumUsers(ownerId))
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async restoreAll(userId: string): Promise<void> {
|
||||
await this.db.updateTable('album').set({ deletedAt: null }).where('ownerId', '=', userId).execute();
|
||||
await this.db.updateTable('album').set({ deletedAt: null }).where(isAlbumOwned(userId)).execute();
|
||||
}
|
||||
|
||||
async softDeleteAll(userId: string): Promise<void> {
|
||||
await this.db.updateTable('album').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute();
|
||||
await this.db.updateTable('album').set({ deletedAt: new Date() }).where(isAlbumOwned(userId)).execute();
|
||||
}
|
||||
|
||||
async deleteAll(userId: string): Promise<void> {
|
||||
await this.db.deleteFrom('album').where('ownerId', '=', userId).execute();
|
||||
await this.db.deleteFrom('album').where(isAlbumOwned(userId)).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@ -306,52 +331,86 @@ export class AlbumRepository {
|
||||
.then((results) => new Set(results.map(({ assetId }) => assetId)));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||
await this.addAssets(this.db, albumId, assetIds);
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.insertInto('album_asset')
|
||||
.expression((eb) =>
|
||||
eb.selectFrom(dummy).select([asUuid(albumId).as('albumId'), sql`unnest(${assetIds}::uuid[])`.as('assetId')]),
|
||||
)
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(album: Insertable<AlbumTable>, assetIds: string[], albumUsers: AlbumUserCreateDto[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newAlbum = await tx.insertInto('album').values(album).returning('album.id').executeTakeFirst();
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{ albumName: DummyValue.STRING },
|
||||
[],
|
||||
[{ userId: DummyValue.UUID, role: AlbumUserRole.Owner }, DummyValue.UUID],
|
||||
],
|
||||
})
|
||||
async create(
|
||||
album: Insertable<AlbumTable>,
|
||||
assetIds: string[],
|
||||
albumUsers: AlbumUserCreateDto[],
|
||||
authUserId: string,
|
||||
) {
|
||||
if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) {
|
||||
throw new Error('Album must have an owner');
|
||||
}
|
||||
|
||||
if (!newAlbum) {
|
||||
throw new Error('Failed to create album');
|
||||
}
|
||||
const userIds = albumUsers.map((u) => u.userId);
|
||||
const roles = albumUsers.map((u) => u.role);
|
||||
|
||||
if (assetIds.length > 0) {
|
||||
await this.addAssets(tx, newAlbum.id, assetIds);
|
||||
}
|
||||
|
||||
if (albumUsers.length > 0) {
|
||||
await tx
|
||||
const result = await this.db
|
||||
.with('album', (db) => db.insertInto('album').values(album).returningAll())
|
||||
.with('album_user', (db) =>
|
||||
db
|
||||
.insertInto('album_user')
|
||||
.values(
|
||||
albumUsers.map((albumUser) => ({ albumId: newAlbum.id, userId: albumUser.userId, role: albumUser.role })),
|
||||
.expression((eb) =>
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(({ ref }) => [
|
||||
ref('album.id').as('albumId'),
|
||||
sql`unnest(${userIds}::uuid[])`.as('userId'),
|
||||
sql`unnest(${roles}::album_user_role_enum[])`.as('role'),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
.returning(['album_user.albumId', 'album_user.userId', 'album_user.role']),
|
||||
)
|
||||
.with('album_asset', (db) =>
|
||||
db
|
||||
.insertInto('album_asset')
|
||||
.expression((eb) =>
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(({ ref }) => [ref('album.id').as('albumId'), sql`unnest(${assetIds}::uuid[])`.as('assetId')]),
|
||||
)
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.returning(['album_asset.albumId', 'album_asset.assetId']),
|
||||
)
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.select(withAlbumUsers(authUserId))
|
||||
.select(withAssets)
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return tx
|
||||
.selectFrom('album')
|
||||
.selectAll('album')
|
||||
.where('id', '=', newAlbum.id)
|
||||
.select(withOwner)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
update(id: string, album: Updateable<AlbumTable>) {
|
||||
update(id: string, album: Updateable<AlbumTable>, authUserId: string) {
|
||||
return this.db
|
||||
.updateTable('album')
|
||||
.set(album)
|
||||
.where('id', '=', id)
|
||||
.where('album.id', '=', id)
|
||||
.returningAll('album')
|
||||
.returning(withOwner)
|
||||
.returning(withSharedLink)
|
||||
.returning(withAlbumUsers)
|
||||
.returning(withAlbumUsers(authUserId))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@ -359,19 +418,6 @@ export class AlbumRepository {
|
||||
await this.db.deleteFrom('album').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 2, chunkSize: 30_000 })
|
||||
private async addAssets(db: Kysely<DB>, albumId: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.insertInto('album_asset')
|
||||
.values(assetIds.map((assetId) => ({ albumId, assetId })))
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Chunked({ chunkSize: 30_000 })
|
||||
async addAssetIdsToAlbums(values: { albumId: string; assetId: string }[]): Promise<void> {
|
||||
if (values.length === 0) {
|
||||
@ -402,7 +448,7 @@ export class AlbumRepository {
|
||||
albumThumbnailAssetId: this.updateThumbnailBuilder(eb)
|
||||
.select('album_asset.assetId')
|
||||
.orderBy('asset.fileCreatedAt', 'desc')
|
||||
.limit(1),
|
||||
.limit(sql.lit(1)),
|
||||
}))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
||||
@ -39,7 +39,7 @@ type EventMap = {
|
||||
|
||||
// album events
|
||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||
AlbumInvite: [{ id: string; userId: string }];
|
||||
AlbumInvite: [{ id: string; userId: string; senderName: string }];
|
||||
|
||||
// asset events
|
||||
AssetCreate: [{ asset: Asset }];
|
||||
|
||||
@ -9,7 +9,7 @@ import { DB } from 'src/schema';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
import { removeUndefinedKeys, withFilePath } from 'src/utils/database';
|
||||
import { dummy, removeUndefinedKeys, withFilePath } from 'src/utils/database';
|
||||
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
|
||||
|
||||
export interface PersonSearchOptions {
|
||||
@ -418,7 +418,7 @@ export class PersonRepository {
|
||||
(query as any) = query.with('added_embeddings', (db) => db.insertInto('face_search').values(embeddingsToAdd));
|
||||
}
|
||||
|
||||
await query.selectFrom(sql`(select 1)`.as('dummy')).execute();
|
||||
await query.selectFrom(dummy).execute();
|
||||
}
|
||||
|
||||
async update(person: Updateable<PersonTable> & { id: string }) {
|
||||
|
||||
@ -5,7 +5,7 @@ import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Album, columns } from 'src/database';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { AlbumUserRole, SharedLinkType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
@ -39,7 +39,15 @@ const withAlbumOwner = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
return eb
|
||||
.selectFrom('user')
|
||||
.select(columns.user)
|
||||
.whereRef('user.id', '=', 'album.ownerId')
|
||||
.where((eb) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.where('album_user.role', '=', sql.lit(AlbumUserRole.Owner))
|
||||
.whereRef('album_user.albumId', '=', 'album.id')
|
||||
.whereRef('album_user.userId', '=', 'user.id'),
|
||||
),
|
||||
)
|
||||
.where('user.deletedAt', 'is', null)
|
||||
.as('owner');
|
||||
};
|
||||
|
||||
@ -171,10 +171,9 @@ class AlbumSync extends BaseSync {
|
||||
return this.upsertQuery('album', options)
|
||||
.distinctOn(['album.id', 'album.updateId'])
|
||||
.leftJoin('album_user as album_users', 'album.id', 'album_users.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_users.userId', '=', userId)]))
|
||||
.where('album_users.userId', '=', userId)
|
||||
.select([
|
||||
'album.id',
|
||||
'album.ownerId',
|
||||
'album.albumName as name',
|
||||
'album.description',
|
||||
'album.createdAt',
|
||||
@ -186,6 +185,11 @@ class AlbumSync extends BaseSync {
|
||||
])
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getAlbumUsers(albumId: string) {
|
||||
return this.db.selectFrom('album_user').select(['userId', 'role']).where('albumId', '=', albumId).execute();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumAssetSync extends BaseSync {
|
||||
@ -207,9 +211,8 @@ class AlbumAssetSync extends BaseSync {
|
||||
.select(columns.syncAsset)
|
||||
.select('asset.updateId')
|
||||
.where('album_asset.updateId', '<=', albumToAssetAck.updateId) // Ensure we only send updates for assets that the client already knows about
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.userId', '=', userId)]))
|
||||
.innerJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where('album_user.userId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
|
||||
@ -220,9 +223,8 @@ class AlbumAssetSync extends BaseSync {
|
||||
.select('album_asset.updateId')
|
||||
.innerJoin('asset', 'asset.id', 'album_asset.assetId')
|
||||
.select(columns.syncAsset)
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.userId', '=', userId)]))
|
||||
.innerJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where('album_user.userId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
@ -246,9 +248,8 @@ class AlbumAssetExifSync extends BaseSync {
|
||||
.select(columns.syncAssetExif)
|
||||
.select('asset_exif.updateId')
|
||||
.where('album_asset.updateId', '<=', albumToAssetAck.updateId) // Ensure we only send exif updates for assets that the client already knows about
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.userId', '=', userId)]))
|
||||
.innerJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where('album_user.userId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
|
||||
@ -261,7 +262,7 @@ class AlbumAssetExifSync extends BaseSync {
|
||||
.select(columns.syncAssetExif)
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.userId', '=', userId)]))
|
||||
.where('album_user.userId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
@ -284,18 +285,7 @@ class AlbumToAssetSync extends BaseSync {
|
||||
eb(
|
||||
'albumId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(['id'])
|
||||
.where('ownerId', '=', userId)
|
||||
.union((eb) =>
|
||||
eb.parens(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select(['album_user.albumId as id'])
|
||||
.where('album_user.userId', '=', userId),
|
||||
),
|
||||
),
|
||||
eb.selectFrom('album_user').select(['album_user.albumId as id']).where('album_user.userId', '=', userId),
|
||||
),
|
||||
)
|
||||
.stream();
|
||||
@ -310,9 +300,8 @@ class AlbumToAssetSync extends BaseSync {
|
||||
const userId = options.userId;
|
||||
return this.upsertQuery('album_asset', options)
|
||||
.select(['album_asset.assetId as assetId', 'album_asset.albumId as albumId', 'album_asset.updateId'])
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.leftJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where((eb) => eb.or([eb('album.ownerId', '=', userId), eb('album_user.userId', '=', userId)]))
|
||||
.innerJoin('album_user', 'album_user.albumId', 'album_asset.albumId')
|
||||
.where('album_user.userId', '=', userId)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
@ -336,18 +325,7 @@ class AlbumUserSync extends BaseSync {
|
||||
eb(
|
||||
'albumId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(['id'])
|
||||
.where('ownerId', '=', userId)
|
||||
.union((eb) =>
|
||||
eb.parens(
|
||||
eb
|
||||
.selectFrom('album_user')
|
||||
.select(['album_user.albumId as id'])
|
||||
.where('album_user.userId', '=', userId),
|
||||
),
|
||||
),
|
||||
eb.selectFrom('album_user').select(['album_user.albumId as id']).where('album_user.userId', '=', userId),
|
||||
),
|
||||
)
|
||||
.stream();
|
||||
@ -368,17 +346,9 @@ class AlbumUserSync extends BaseSync {
|
||||
'album_user.albumId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(['id'])
|
||||
.where('ownerId', '=', userId)
|
||||
.union((eb) =>
|
||||
eb.parens(
|
||||
eb
|
||||
.selectFrom('album_user as albumUsers')
|
||||
.select(['albumUsers.albumId as id'])
|
||||
.where('albumUsers.userId', '=', userId),
|
||||
),
|
||||
),
|
||||
.selectFrom('album_user as albumUsers')
|
||||
.select(['albumUsers.albumId as id'])
|
||||
.where('albumUsers.userId', '=', userId),
|
||||
),
|
||||
)
|
||||
.stream();
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { registerEnum } from '@immich/sql-tools';
|
||||
import { AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
|
||||
import { AlbumUserRole, AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
|
||||
|
||||
export const album_user_role_enum = registerEnum({
|
||||
name: 'album_user_role_enum',
|
||||
values: [AlbumUserRole.Owner, AlbumUserRole.Editor, AlbumUserRole.Viewer],
|
||||
});
|
||||
|
||||
export const assets_status_enum = registerEnum({
|
||||
name: 'assets_status_enum',
|
||||
|
||||
@ -29,7 +29,8 @@ export const album_user_after_insert = registerFunction({
|
||||
body: `
|
||||
BEGIN
|
||||
UPDATE album SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||
WHERE "id" IN (SELECT DISTINCT "albumId" FROM inserted_rows);
|
||||
WHERE "id" IN (SELECT "albumId" FROM inserted_rows)
|
||||
AND NOT EXISTS (SELECT FROM inserted_rows WHERE role = 'owner');
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
@ -119,19 +120,6 @@ export const asset_delete_audit = registerFunction({
|
||||
END`,
|
||||
});
|
||||
|
||||
export const album_delete_audit = registerFunction({
|
||||
name: 'album_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO album_audit ("albumId", "userId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
});
|
||||
|
||||
export const album_asset_delete_audit = registerFunction({
|
||||
name: 'album_asset_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Database, Extensions, Generated, Int8 } from '@immich/sql-tools';
|
||||
import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
||||
import {
|
||||
album_delete_audit,
|
||||
album_user_role_enum,
|
||||
asset_face_source_type,
|
||||
asset_visibility_enum,
|
||||
assets_status_enum,
|
||||
} from 'src/schema/enums';
|
||||
import {
|
||||
album_user_after_insert,
|
||||
album_user_delete_audit,
|
||||
asset_delete_audit,
|
||||
@ -146,7 +150,6 @@ export class ImmichDatabase {
|
||||
user_delete_audit,
|
||||
partner_delete_audit,
|
||||
asset_delete_audit,
|
||||
album_delete_audit,
|
||||
album_user_after_insert,
|
||||
album_user_delete_audit,
|
||||
memory_delete_audit,
|
||||
@ -158,7 +161,7 @@ export class ImmichDatabase {
|
||||
asset_face_audit,
|
||||
];
|
||||
|
||||
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||
enum = [album_user_role_enum, assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||
}
|
||||
|
||||
export interface Migrations {
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION album_user_after_insert()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE album SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||
WHERE "id" IN (SELECT "albumId" FROM inserted_rows)
|
||||
AND NOT EXISTS (SELECT FROM inserted_rows WHERE role = 'owner');
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`DROP TRIGGER "album_delete_audit" ON "album";`.execute(db);
|
||||
await sql`DROP FUNCTION album_delete_audit;`.execute(db);
|
||||
await sql`CREATE TYPE "album_user_role_enum" AS ENUM ('owner','editor','viewer');`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" DROP DEFAULT;`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" TYPE album_user_role_enum USING "role"::album_user_role_enum;`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" SET DEFAULT 'editor'::album_user_role_enum;`.execute(db);
|
||||
await db
|
||||
.insertInto('album_user')
|
||||
.expression((eb) =>
|
||||
eb
|
||||
.selectFrom('album')
|
||||
.select(['album.id as albumId', 'album.ownerId as userId', eb.val(AlbumUserRole.Owner).as('role')]),
|
||||
)
|
||||
.execute();
|
||||
await sql`ALTER TABLE "album" DROP CONSTRAINT "album_ownerId_fkey";`.execute(db);
|
||||
await sql`ALTER TABLE "album" DROP COLUMN "ownerId";`.execute(db);
|
||||
await sql`CREATE UNIQUE INDEX "album_user_unique_owner" ON "album_user" ("albumId") WHERE (role = 'owner');`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"album_user_after_insert","sql":"CREATE OR REPLACE FUNCTION album_user_after_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE album SET \\"updatedAt\\" = clock_timestamp(), \\"updateId\\" = immich_uuid_v7(clock_timestamp())\\n WHERE \\"id\\" IN (SELECT \\"albumId\\" FROM inserted_rows)\\n AND NOT EXISTS (SELECT FROM inserted_rows WHERE role = ''owner'');\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_album_user_after_insert';`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_album_user_unique_owner', '{"type":"index","name":"album_user_unique_owner","sql":"CREATE UNIQUE INDEX \\"album_user_unique_owner\\" ON \\"album_user\\" (\\"albumId\\") WHERE (role = ''owner'');"}'::jsonb);`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_delete_audit';`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_delete_audit';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION public.album_user_after_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE album SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||
WHERE "id" IN (SELECT DISTINCT "albumId" FROM inserted_rows);
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION public.album_delete_audit()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
INSERT INTO album_audit ("albumId", "userId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$function$
|
||||
`.execute(db);
|
||||
await sql`ALTER TABLE "album" ADD "ownerId" uuid NOT NULL;`.execute(db);
|
||||
await db
|
||||
.updateTable('album')
|
||||
.set((eb) =>
|
||||
({
|
||||
id: eb.ref('album_user.albumId'),
|
||||
ownerId: eb.ref('album_user.userId')
|
||||
})
|
||||
)
|
||||
.from('album_user')
|
||||
.where('album_user.role', '=', AlbumUserRole.Owner)
|
||||
.execute();
|
||||
await sql`DROP INDEX "album_user_unique_owner";`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" DROP DEFAULT;`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" TYPE character varying USING "role"::text;`.execute(db);
|
||||
await sql`ALTER TABLE "album_user" ALTER COLUMN "role" SET DEFAULT 'editor';`.execute(db);
|
||||
await sql`DROP TYPE "album_user_role_enum";`.execute(db);
|
||||
await sql`CREATE INDEX "album_ownerId_idx" ON "album" ("ownerId");`.execute(db);
|
||||
await sql`ALTER TABLE "album" ADD CONSTRAINT "album_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_delete_audit"
|
||||
AFTER DELETE ON "album"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN ((pg_trigger_depth() = 0))
|
||||
EXECUTE FUNCTION album_delete_audit();`.execute(db);
|
||||
await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION album_user_after_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE album SET \\"updatedAt\\" = clock_timestamp(), \\"updateId\\" = immich_uuid_v7(clock_timestamp())\\n WHERE \\"id\\" IN (SELECT DISTINCT \\"albumId\\" FROM inserted_rows);\\n RETURN NULL;\\n END\\n $$;","name":"album_user_after_insert","type":"function"}'::jsonb WHERE "name" = 'function_album_user_after_insert';`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_delete_audit', '{"sql":"CREATE OR REPLACE FUNCTION album_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO album_audit (\\"albumId\\", \\"userId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;","name":"album_delete_audit","type":"function"}'::jsonb);`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_delete_audit', '{"sql":"CREATE OR REPLACE TRIGGER \\"album_delete_audit\\"\\n AFTER DELETE ON \\"album\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION album_delete_audit();","name":"album_delete_audit","type":"trigger"}'::jsonb);`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_album_user_unique_owner';`.execute(db);
|
||||
}
|
||||
@ -5,17 +5,25 @@ import {
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Index,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { album_user_role_enum } from 'src/schema/enums';
|
||||
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
|
||||
@Table({ name: 'album_user' })
|
||||
@Index({
|
||||
name: 'album_user_unique_owner',
|
||||
columns: ['albumId'],
|
||||
unique: true,
|
||||
where: `role = 'owner'`,
|
||||
})
|
||||
// Pre-existing indices from original album <--> user ManyToMany mapping
|
||||
@UpdatedAtTrigger('album_user_updatedAt')
|
||||
@AfterInsertTrigger({
|
||||
@ -47,7 +55,7 @@ export class AlbumUserTable {
|
||||
})
|
||||
userId!: string;
|
||||
|
||||
@Column({ type: 'character varying', default: AlbumUserRole.Editor })
|
||||
@Column({ enum: album_user_role_enum, default: AlbumUserRole.Editor })
|
||||
role!: Generated<AlbumUserRole>;
|
||||
|
||||
@CreateIdColumn({ index: true })
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
AfterDeleteTrigger,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
@ -12,25 +11,14 @@ import {
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
import { album_delete_audit } from 'src/schema/functions';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
|
||||
@Table({ name: 'album' })
|
||||
@UpdatedAtTrigger('album_updatedAt')
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'statement',
|
||||
function: album_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() = 0',
|
||||
})
|
||||
export class AlbumTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
ownerId!: string;
|
||||
|
||||
@Column({ default: 'Untitled Album' })
|
||||
albumName!: Generated<string>;
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ describe(AlbumService.name, () => {
|
||||
describe('getAll', () => {
|
||||
it('gets list of albums for auth user', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const sharedWithUserAlbum = AlbumFactory.from().owner(owner).albumUser().build();
|
||||
mocks.album.getOwned.mockResolvedValue([getForAlbum(album), getForAlbum(sharedWithUserAlbum)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -63,7 +64,7 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(AuthFactory.create(album.owner), {});
|
||||
const result = await sut.getAll(AuthFactory.create(owner), {});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toEqual(album.id);
|
||||
expect(result[1].id).toEqual(sharedWithUserAlbum.id);
|
||||
@ -76,6 +77,7 @@ describe(AlbumService.name, () => {
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.asset({}, (builder) => builder.exif())
|
||||
.build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -87,7 +89,7 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(AuthFactory.create(album.owner), { assetId: album.assets[0].id });
|
||||
const result = await sut.getAll(AuthFactory.create(owner), { assetId: album.assets[0].id });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(album.id);
|
||||
expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(1);
|
||||
@ -95,6 +97,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('gets list of albums that are shared', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getShared.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -106,7 +109,7 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(AuthFactory.create(album.owner), { shared: true });
|
||||
const result = await sut.getAll(AuthFactory.create(owner), { shared: true });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(album.id);
|
||||
expect(mocks.album.getShared).toHaveBeenCalledTimes(1);
|
||||
@ -114,6 +117,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('gets list of albums that are NOT shared', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getNotShared.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -125,7 +129,7 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(AuthFactory.create(album.owner), { shared: false });
|
||||
const result = await sut.getAll(AuthFactory.create(owner), { shared: false });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(album.id);
|
||||
expect(mocks.album.getNotShared).toHaveBeenCalledTimes(1);
|
||||
@ -134,6 +138,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('counts assets correctly', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getOwned.mockResolvedValue([getForAlbum(album)]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
{
|
||||
@ -145,7 +150,7 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sut.getAll(AuthFactory.create(album.owner), {});
|
||||
const result = await sut.getAll(AuthFactory.create(owner), {});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].assetCount).toEqual(1);
|
||||
expect(mocks.album.getOwned).toHaveBeenCalledTimes(1);
|
||||
@ -159,13 +164,14 @@ describe(AlbumService.name, () => {
|
||||
.asset({ id: assetId }, (asset) => asset.exif())
|
||||
.albumUser(albumUser)
|
||||
.build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user));
|
||||
mocks.user.getMetadata.mockResolvedValue([]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
|
||||
await sut.create(AuthFactory.create(album.owner), {
|
||||
await sut.create(AuthFactory.create(owner), {
|
||||
albumName: 'test',
|
||||
albumUsers: [albumUser],
|
||||
description: 'description',
|
||||
@ -174,20 +180,27 @@ describe(AlbumService.name, () => {
|
||||
|
||||
expect(mocks.album.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: album.owner.id,
|
||||
albumName: 'test',
|
||||
description: 'description',
|
||||
order: album.order,
|
||||
albumThumbnailAssetId: assetId,
|
||||
},
|
||||
[assetId],
|
||||
[{ userId: albumUser.userId, role: AlbumUserRole.Editor }],
|
||||
[
|
||||
{ userId: owner.id, role: AlbumUserRole.Owner },
|
||||
{ userId: albumUser.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
owner.id,
|
||||
);
|
||||
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId });
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: album.id,
|
||||
userId: albumUser.userId,
|
||||
senderName: owner.name,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates album with assetOrder from user preferences', async () => {
|
||||
@ -197,8 +210,10 @@ describe(AlbumService.name, () => {
|
||||
.asset({ id: assetId }, (asset) => asset.exif())
|
||||
.albumUser(albumUser)
|
||||
.build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||
mocks.albumUser.create.mockResolvedValue(album.albumUsers[0]);
|
||||
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[1].user));
|
||||
mocks.user.getMetadata.mockResolvedValue([
|
||||
{
|
||||
key: UserMetadataKey.Preferences,
|
||||
@ -211,7 +226,7 @@ describe(AlbumService.name, () => {
|
||||
]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
|
||||
await sut.create(AuthFactory.create(album.owner), {
|
||||
await sut.create(AuthFactory.create(owner), {
|
||||
albumName: album.albumName,
|
||||
albumUsers: [albumUser],
|
||||
description: album.description,
|
||||
@ -220,20 +235,24 @@ describe(AlbumService.name, () => {
|
||||
|
||||
expect(mocks.album.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: album.owner.id,
|
||||
albumName: album.albumName,
|
||||
description: album.description,
|
||||
order: 'asc',
|
||||
albumThumbnailAssetId: assetId,
|
||||
},
|
||||
[assetId],
|
||||
[albumUser],
|
||||
[{ userId: owner.id, role: AlbumUserRole.Owner }, albumUser],
|
||||
owner.id,
|
||||
);
|
||||
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId });
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: album.id,
|
||||
userId: albumUser.userId,
|
||||
senderName: owner.name,
|
||||
});
|
||||
});
|
||||
|
||||
it('should require valid userIds', async () => {
|
||||
@ -254,12 +273,13 @@ describe(AlbumService.name, () => {
|
||||
.asset({ id: assetId }, (asset) => asset.exif())
|
||||
.albumUser()
|
||||
.build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
|
||||
mocks.album.create.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.getMetadata.mockResolvedValue([]);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
|
||||
|
||||
await sut.create(AuthFactory.create(album.owner), {
|
||||
await sut.create(AuthFactory.create(owner), {
|
||||
albumName: album.albumName,
|
||||
description: album.description,
|
||||
assetIds: [assetId, 'asset-2'],
|
||||
@ -267,29 +287,26 @@ describe(AlbumService.name, () => {
|
||||
|
||||
expect(mocks.album.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: album.owner.id,
|
||||
albumName: album.albumName,
|
||||
description: album.description,
|
||||
order: 'desc',
|
||||
albumThumbnailAssetId: assetId,
|
||||
},
|
||||
[assetId],
|
||||
[],
|
||||
);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
album.owner.id,
|
||||
new Set([assetId, 'asset-2']),
|
||||
false,
|
||||
[{ userId: owner.id, role: AlbumUserRole.Owner }],
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId, 'asset-2']), false);
|
||||
});
|
||||
|
||||
it('should throw an error if the userId is the ownerId', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
mocks.user.get.mockResolvedValue(album.owner);
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.user.get.mockResolvedValue(owner);
|
||||
await expect(
|
||||
sut.create(AuthFactory.create(album.owner), {
|
||||
sut.create(AuthFactory.create(owner), {
|
||||
albumName: 'Empty album',
|
||||
albumUsers: [{ userId: album.owner.id, role: AlbumUserRole.Editor }],
|
||||
albumUsers: [{ userId: owner.id, role: AlbumUserRole.Editor }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.album.create).not.toHaveBeenCalled();
|
||||
@ -312,20 +329,22 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should prevent updating a not owned album (shared with auth user)', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
await expect(
|
||||
sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' }),
|
||||
sut.update(AuthFactory.create(owner), album.id, { albumName: 'new album name' }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should require a valid thumbnail asset id', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.update(AuthFactory.create(album.owner), album.id, { albumThumbnailAssetId: 'not-in-album' }),
|
||||
sut.update(AuthFactory.create(owner), album.id, { albumThumbnailAssetId: 'not-in-album' }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.album.getAssetIds).toHaveBeenCalledWith(album.id, ['not-in-album']);
|
||||
@ -334,43 +353,51 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should allow the owner to update the album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' });
|
||||
await sut.update(AuthFactory.create(owner), album.id, { albumName: 'new album name' });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(album.id, { id: album.id, albumName: 'new album name' });
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||
album.id,
|
||||
{ id: album.id, albumName: 'new album name' },
|
||||
owner.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should require permissions', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.delete(AuthFactory.create(owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.album.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not let a shared user delete the album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.delete(AuthFactory.create(owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.album.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should let the owner delete an album', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await sut.delete(AuthFactory.create(album.owner), album.id);
|
||||
await sut.delete(AuthFactory.create(owner), album.id);
|
||||
|
||||
expect(mocks.album.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.delete).toHaveBeenCalledWith(album.id);
|
||||
@ -391,10 +418,11 @@ describe(AlbumService.name, () => {
|
||||
it('should throw an error if the userId is already added', async () => {
|
||||
const userId = newUuid();
|
||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }),
|
||||
sut.addUsers(AuthFactory.create(owner), album.id, { albumUsers: [{ userId }] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).not.toHaveBeenCalled();
|
||||
@ -402,11 +430,12 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should throw an error if the userId does not exist', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }),
|
||||
sut.addUsers(AuthFactory.create(owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('unknown-user', {});
|
||||
@ -414,11 +443,12 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should throw an error if the userId is the ownerId', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
await expect(
|
||||
sut.addUsers(AuthFactory.create(album.owner), album.id, {
|
||||
albumUsers: [{ userId: album.owner.id }],
|
||||
sut.addUsers(AuthFactory.create(owner), album.id, {
|
||||
albumUsers: [{ userId: owner.id }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
@ -427,6 +457,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should add valid shared users', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const user = UserFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
@ -434,7 +465,7 @@ describe(AlbumService.name, () => {
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
||||
|
||||
await sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: user.id }] });
|
||||
await sut.addUsers(AuthFactory.create(owner), album.id, { albumUsers: [{ userId: user.id }] });
|
||||
|
||||
expect(mocks.albumUser.create).toHaveBeenCalledWith({
|
||||
userId: user.id,
|
||||
@ -443,6 +474,7 @@ describe(AlbumService.name, () => {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: album.id,
|
||||
userId: user.id,
|
||||
senderName: owner.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -460,15 +492,16 @@ describe(AlbumService.name, () => {
|
||||
it('should remove a shared user from an owned album', async () => {
|
||||
const userId = newUuid();
|
||||
const album = AlbumFactory.from().albumUser({ userId }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.albumUser.delete.mockResolvedValue();
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined();
|
||||
await expect(sut.removeUser(AuthFactory.create(owner), album.id, userId)).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId });
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }, owner.id);
|
||||
});
|
||||
|
||||
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
|
||||
@ -511,9 +544,10 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should not allow the owner to be removed', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf(
|
||||
await expect(sut.removeUser(AuthFactory.create(owner), album.id, owner.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
@ -522,9 +556,10 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should throw an error for a user not in the album', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
|
||||
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf(
|
||||
await expect(sut.removeUser(AuthFactory.create(owner), album.id, 'user-3')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
@ -536,10 +571,11 @@ describe(AlbumService.name, () => {
|
||||
it('should update user role', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.albumUser.update.mockResolvedValue();
|
||||
|
||||
await sut.updateUser(AuthFactory.create(album.owner), album.id, user.id, { role: AlbumUserRole.Viewer });
|
||||
await sut.updateUser(AuthFactory.create(owner), album.id, user.id, { role: AlbumUserRole.Viewer });
|
||||
|
||||
expect(mocks.albumUser.update).toHaveBeenCalledWith(
|
||||
{ albumId: album.id, userId: user.id },
|
||||
@ -551,6 +587,7 @@ describe(AlbumService.name, () => {
|
||||
describe('getAlbumInfo', () => {
|
||||
it('should get a shared album', async () => {
|
||||
const album = AlbumFactory.from().albumUser().build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getMetadataForIds.mockResolvedValue([
|
||||
@ -563,10 +600,10 @@ describe(AlbumService.name, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.get(AuthFactory.create(album.owner), album.id);
|
||||
await sut.get(AuthFactory.create(owner), album.id);
|
||||
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id]));
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }, owner.id);
|
||||
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([album.id]));
|
||||
});
|
||||
|
||||
it('should get a shared album via a shared link', async () => {
|
||||
@ -586,7 +623,7 @@ describe(AlbumService.name, () => {
|
||||
const auth = AuthFactory.from().sharedLink().build();
|
||||
await sut.get(auth, album.id);
|
||||
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }, auth.user.id);
|
||||
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id]));
|
||||
});
|
||||
|
||||
@ -607,7 +644,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
await sut.get(AuthFactory.create(user), album.id);
|
||||
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
|
||||
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }, user.id);
|
||||
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
new Set([album.id]),
|
||||
@ -631,7 +668,7 @@ describe(AlbumService.name, () => {
|
||||
describe('addAssets', () => {
|
||||
it('should allow the owner to add assets', async () => {
|
||||
const owner = UserFactory.create({ isAdmin: true });
|
||||
const album = AlbumFactory.from({ ownerId: owner.id }).owner(owner).build();
|
||||
const album = AlbumFactory.from().owner(owner).build();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
@ -646,37 +683,47 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: asset3.id },
|
||||
]);
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(album.id, {
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||
album.id,
|
||||
{
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]);
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
const [asset1, asset2] = [AssetFactory.create(), AssetFactory.create()];
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([
|
||||
await expect(sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset2.id] })).resolves.toEqual([
|
||||
{ success: true, id: asset2.id },
|
||||
]);
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(album.id, {
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||
album.id,
|
||||
{
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow a shared user to add assets', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
@ -691,15 +738,19 @@ describe(AlbumService.name, () => {
|
||||
{ success: true, id: asset3.id },
|
||||
]);
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(album.id, {
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||
album.id,
|
||||
{
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
user.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album.id,
|
||||
recipientId: album.ownerId,
|
||||
recipientId: owner.id,
|
||||
});
|
||||
});
|
||||
|
||||
@ -719,33 +770,39 @@ describe(AlbumService.name, () => {
|
||||
|
||||
it('should allow adding assets shared via partner sharing', async () => {
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: true, id: asset.id },
|
||||
]);
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(album.id, {
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
});
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id]));
|
||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||
album.id,
|
||||
{
|
||||
id: album.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]));
|
||||
});
|
||||
|
||||
it('should skip duplicate assets', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id]));
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: false, id: asset.id, error: BulkIdErrorReason.DUPLICATE },
|
||||
]);
|
||||
|
||||
@ -755,16 +812,17 @@ describe(AlbumService.name, () => {
|
||||
it('should skip assets not shared with user', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: false, id: asset.id, error: BulkIdErrorReason.NO_PERMISSION },
|
||||
]);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id]), false);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id]));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]), false);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]));
|
||||
});
|
||||
|
||||
it('should not allow unauthorized access to the album', async () => {
|
||||
@ -797,6 +855,7 @@ describe(AlbumService.name, () => {
|
||||
describe('addAssetsToAlbums', () => {
|
||||
it('should allow the owner to add assets', async () => {
|
||||
const album1 = AlbumFactory.create();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.create();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
@ -805,23 +864,33 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
).resolves.toEqual({ success: true, error: undefined });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, {
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, {
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
album1.id,
|
||||
{
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
album2.id,
|
||||
{
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||
{ albumId: album1.id, assetId: asset1.id },
|
||||
{ albumId: album1.id, assetId: asset2.id },
|
||||
@ -835,6 +904,7 @@ describe(AlbumService.name, () => {
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album1 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build();
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
@ -843,23 +913,33 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
).resolves.toEqual({ success: true, error: undefined });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, {
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, {
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
album1.id,
|
||||
{
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
album2.id,
|
||||
{
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||
{ albumId: album1.id, assetId: asset1.id },
|
||||
{ albumId: album1.id, assetId: asset2.id },
|
||||
@ -873,7 +953,9 @@ describe(AlbumService.name, () => {
|
||||
it('should allow a shared user to add assets', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album1 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build();
|
||||
const { user: owner1 } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build();
|
||||
const { user: owner2 } = album2.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
@ -888,16 +970,26 @@ describe(AlbumService.name, () => {
|
||||
).resolves.toEqual({ success: true, error: undefined });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, {
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, {
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
album1.id,
|
||||
{
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
user.id,
|
||||
);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
album2.id,
|
||||
{
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
user.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||
{ albumId: album1.id, assetId: asset1.id },
|
||||
{ albumId: album1.id, assetId: asset2.id },
|
||||
@ -908,11 +1000,11 @@ describe(AlbumService.name, () => {
|
||||
]);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album1.id,
|
||||
recipientId: album1.ownerId,
|
||||
recipientId: owner1.id,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: album2.id,
|
||||
recipientId: album2.ownerId,
|
||||
recipientId: owner2.id,
|
||||
});
|
||||
});
|
||||
|
||||
@ -942,6 +1034,7 @@ describe(AlbumService.name, () => {
|
||||
it('should allow adding assets shared via partner sharing', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album1 = AlbumFactory.create();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.create();
|
||||
const [asset1, asset2, asset3] = [
|
||||
AssetFactory.create({ ownerId: user.id }),
|
||||
@ -954,23 +1047,33 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
).resolves.toEqual({ success: true, error: undefined });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, {
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, {
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
album1.id,
|
||||
{
|
||||
id: album1.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
album2.id,
|
||||
{
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||
{ albumId: album1.id, assetId: asset1.id },
|
||||
{ albumId: album1.id, assetId: asset2.id },
|
||||
@ -980,7 +1083,7 @@ describe(AlbumService.name, () => {
|
||||
{ albumId: album2.id, assetId: asset3.id },
|
||||
]);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||
album1.ownerId,
|
||||
owner.id,
|
||||
new Set([asset1.id, asset2.id, asset3.id]),
|
||||
);
|
||||
});
|
||||
@ -988,7 +1091,9 @@ describe(AlbumService.name, () => {
|
||||
it('should skip some duplicate assets', async () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
const album1 = AlbumFactory.create();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.create();
|
||||
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
mocks.album.getAssetIds
|
||||
@ -997,18 +1102,23 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
).resolves.toEqual({ success: true, error: undefined });
|
||||
|
||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(1, album2.id, {
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
});
|
||||
expect(mocks.album.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
album2.id,
|
||||
{
|
||||
id: album2.id,
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: asset1.id,
|
||||
},
|
||||
owner.id,
|
||||
);
|
||||
expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([
|
||||
{ albumId: album2.id, assetId: asset1.id },
|
||||
{ albumId: album2.id, assetId: asset2.id },
|
||||
@ -1019,6 +1129,7 @@ describe(AlbumService.name, () => {
|
||||
it('should skip all duplicate assets', async () => {
|
||||
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
|
||||
const album1 = AlbumFactory.create();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.create();
|
||||
mocks.access.album.checkOwnerAccess
|
||||
.mockResolvedValueOnce(new Set([album1.id]))
|
||||
@ -1028,7 +1139,7 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
@ -1044,6 +1155,7 @@ describe(AlbumService.name, () => {
|
||||
it('should skip assets not shared with user', async () => {
|
||||
const user = UserFactory.create();
|
||||
const album1 = AlbumFactory.create();
|
||||
const { user: owner } = album1.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
const album2 = AlbumFactory.create();
|
||||
const [asset1, asset2, asset3] = [
|
||||
AssetFactory.create({ ownerId: user.id }),
|
||||
@ -1057,7 +1169,7 @@ describe(AlbumService.name, () => {
|
||||
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
|
||||
|
||||
await expect(
|
||||
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
|
||||
sut.addAssetsToAlbums(AuthFactory.create(owner), {
|
||||
albumIds: [album1.id, album2.id],
|
||||
assetIds: [asset1.id, asset2.id, asset3.id],
|
||||
}),
|
||||
@ -1069,12 +1181,12 @@ describe(AlbumService.name, () => {
|
||||
expect(mocks.album.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.addAssetIds).not.toHaveBeenCalled();
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
album1.ownerId,
|
||||
owner.id,
|
||||
new Set([asset1.id, asset2.id, asset3.id]),
|
||||
false,
|
||||
);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(
|
||||
album1.ownerId,
|
||||
owner.id,
|
||||
new Set([asset1.id, asset2.id, asset3.id]),
|
||||
);
|
||||
});
|
||||
@ -1126,12 +1238,13 @@ describe(AlbumService.name, () => {
|
||||
it('should allow the owner to remove assets', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.removeAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: true, id: asset.id },
|
||||
]);
|
||||
|
||||
@ -1141,11 +1254,12 @@ describe(AlbumService.name, () => {
|
||||
it('should skip assets not in the album', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.removeAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: false, id: asset.id, error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
@ -1155,11 +1269,12 @@ describe(AlbumService.name, () => {
|
||||
it('should allow owner to remove all assets from the album', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const album = AlbumFactory.create();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
await expect(sut.removeAssets(AuthFactory.create(owner), album.id, { ids: [asset.id] })).resolves.toEqual([
|
||||
{ success: true, id: asset.id },
|
||||
]);
|
||||
});
|
||||
@ -1168,12 +1283,13 @@ describe(AlbumService.name, () => {
|
||||
const asset1 = AssetFactory.create();
|
||||
const asset2 = AssetFactory.create();
|
||||
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
|
||||
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset1.id] })).resolves.toEqual([
|
||||
await expect(sut.removeAssets(AuthFactory.create(owner), album.id, { ids: [asset1.id] })).resolves.toEqual([
|
||||
{ success: true, id: asset1.id },
|
||||
]);
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerResponseDto } from 'src/dtos/map.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AlbumUserRole, Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
@ -74,10 +74,10 @@ export class AlbumService extends BaseService {
|
||||
async get(auth: AuthDto, id: string): Promise<AlbumResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
|
||||
await this.albumRepository.updateThumbnails();
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: false });
|
||||
const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]);
|
||||
|
||||
const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0;
|
||||
const hasSharedUsers = album.albumUsers && album.albumUsers.length > 1;
|
||||
const hasSharedLink = album.sharedLinks && album.sharedLinks.length > 0;
|
||||
const isShared = hasSharedUsers || hasSharedLink;
|
||||
|
||||
@ -114,6 +114,7 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Cannot share album with owner');
|
||||
}
|
||||
}
|
||||
albumUsers.unshift({ userId: auth.user.id, role: AlbumUserRole.Owner });
|
||||
|
||||
const allowedAssetIdsSet = await this.checkAccess({
|
||||
auth,
|
||||
@ -126,7 +127,6 @@ export class AlbumService extends BaseService {
|
||||
|
||||
const album = await this.albumRepository.create(
|
||||
{
|
||||
ownerId: auth.user.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: assetIds[0] || null,
|
||||
@ -134,10 +134,11 @@ export class AlbumService extends BaseService {
|
||||
},
|
||||
assetIds,
|
||||
albumUsers,
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
for (const { userId } of albumUsers) {
|
||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
|
||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
return mapAlbum(album);
|
||||
@ -146,7 +147,7 @@ export class AlbumService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: true });
|
||||
|
||||
if (dto.albumThumbnailAssetId) {
|
||||
const results = await this.albumRepository.getAssetIds(id, [dto.albumThumbnailAssetId]);
|
||||
@ -154,14 +155,18 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
const updatedAlbum = await this.albumRepository.update(album.id, {
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
isActivityEnabled: dto.isActivityEnabled,
|
||||
order: dto.order,
|
||||
});
|
||||
const updatedAlbum = await this.albumRepository.update(
|
||||
album.id,
|
||||
{
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
isActivityEnabled: dto.isActivityEnabled,
|
||||
order: dto.order,
|
||||
},
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
return mapAlbum({ ...updatedAlbum, assets: album.assets });
|
||||
}
|
||||
@ -172,7 +177,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: false });
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetCreate, ids: [id] });
|
||||
|
||||
const results = await addAssets(
|
||||
@ -183,16 +188,18 @@ export class AlbumService extends BaseService {
|
||||
|
||||
const { id: firstNewAssetId } = results.find(({ success }) => success) || {};
|
||||
if (firstNewAssetId) {
|
||||
await this.albumRepository.update(id, {
|
||||
await this.albumRepository.update(
|
||||
id,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||
});
|
||||
|
||||
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
|
||||
(userId) => userId !== auth.user.id,
|
||||
{
|
||||
id,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||
},
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
const allUsersExceptUs = album.albumUsers.map(({ user }) => user.id).filter((userId) => userId !== auth.user.id);
|
||||
|
||||
for (const recipientId of allUsersExceptUs) {
|
||||
await this.eventRepository.emit('AlbumUpdate', { id, recipientId });
|
||||
}
|
||||
@ -231,21 +238,23 @@ export class AlbumService extends BaseService {
|
||||
if (notPresentAssetIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const album = await this.findOrFail(albumId, { withAssets: false });
|
||||
const album = await this.findOrFail(albumId, auth.user.id, { withAssets: false });
|
||||
results.error = undefined;
|
||||
results.success = true;
|
||||
|
||||
for (const assetId of notPresentAssetIds) {
|
||||
albumAssetValues.push({ albumId, assetId });
|
||||
}
|
||||
await this.albumRepository.update(albumId, {
|
||||
id: albumId,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? notPresentAssetIds[0],
|
||||
});
|
||||
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter(
|
||||
(userId) => userId !== auth.user.id,
|
||||
await this.albumRepository.update(
|
||||
albumId,
|
||||
{
|
||||
id: albumId,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? notPresentAssetIds[0],
|
||||
},
|
||||
auth.user.id,
|
||||
);
|
||||
const allUsersExceptUs = album.albumUsers.map(({ user }) => user.id).filter((userId) => userId !== auth.user.id);
|
||||
events.push({ id: albumId, recipients: allUsersExceptUs });
|
||||
}
|
||||
|
||||
@ -262,7 +271,7 @@ export class AlbumService extends BaseService {
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumAssetDelete, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: false });
|
||||
const results = await removeAssets(
|
||||
auth,
|
||||
{ access: this.accessRepository, bulk: this.albumRepository },
|
||||
@ -280,11 +289,11 @@ export class AlbumService extends BaseService {
|
||||
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: false });
|
||||
|
||||
for (const { userId, role } of albumUsers) {
|
||||
if (album.ownerId === userId) {
|
||||
throw new BadRequestException('Cannot be shared with owner');
|
||||
if (role === AlbumUserRole.Owner) {
|
||||
throw new BadRequestException('Cannot add another owner');
|
||||
}
|
||||
|
||||
const exists = album.albumUsers.find(({ user: { id } }) => id === userId);
|
||||
@ -298,10 +307,10 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
await this.albumUserRepository.create({ userId, albumId: id, role });
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId });
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
return this.findOrFail(id, { withAssets: true }).then(mapAlbum);
|
||||
return this.findOrFail(id, auth.user.id, { withAssets: true }).then(mapAlbum);
|
||||
}
|
||||
|
||||
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
|
||||
@ -309,17 +318,20 @@ export class AlbumService extends BaseService {
|
||||
userId = auth.user.id;
|
||||
}
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
if (album.ownerId === userId) {
|
||||
throw new BadRequestException('Cannot remove album owner');
|
||||
}
|
||||
const album = await this.findOrFail(id, auth.user.id, { withAssets: false });
|
||||
|
||||
const exists = album.albumUsers.find(({ user: { id } }) => id === userId);
|
||||
if (!exists) {
|
||||
throw new BadRequestException('Album not shared with user');
|
||||
}
|
||||
|
||||
if (
|
||||
exists.role === AlbumUserRole.Owner &&
|
||||
album.albumUsers.filter(({ role }) => role === AlbumUserRole.Owner).length === 1
|
||||
) {
|
||||
throw new BadRequestException('Cannot remove the last album owner');
|
||||
}
|
||||
|
||||
// non-admin can remove themselves
|
||||
if (auth.user.id !== userId) {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
|
||||
@ -333,8 +345,8 @@ export class AlbumService extends BaseService {
|
||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||
}
|
||||
|
||||
private async findOrFail(id: string, options: AlbumInfoOptions) {
|
||||
const album = await this.albumRepository.getById(id, options);
|
||||
private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) {
|
||||
const album = await this.albumRepository.getById(id, options, authUserId);
|
||||
if (!album) {
|
||||
throw new BadRequestException('Album not found');
|
||||
}
|
||||
|
||||
@ -168,10 +168,10 @@ describe(NotificationService.name, () => {
|
||||
|
||||
describe('onAlbumInviteEvent', () => {
|
||||
it('should queue notify album invite event', async () => {
|
||||
await sut.onAlbumInvite({ id: '', userId: '42' });
|
||||
await sut.onAlbumInvite({ id: '', userId: '42', senderName: 'foo' });
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.NotifyAlbumInvite,
|
||||
data: { id: '', recipientId: '42' },
|
||||
data: { id: '', recipientId: '42', senderName: 'foo' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -264,14 +264,18 @@ describe(NotificationService.name, () => {
|
||||
|
||||
describe('handleAlbumInvite', () => {
|
||||
it('should skip if album could not be found', async () => {
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Skipped,
|
||||
);
|
||||
expect(mocks.user.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if recipient could not be found', async () => {
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Skipped,
|
||||
);
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -288,7 +292,9 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Skipped,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip if the recipient has email notifications for album invite disabled', async () => {
|
||||
@ -304,7 +310,9 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Skipped,
|
||||
);
|
||||
});
|
||||
|
||||
it('should send invite email', async () => {
|
||||
@ -322,7 +330,9 @@ describe(NotificationService.name, () => {
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SendMail,
|
||||
data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album') }),
|
||||
@ -346,7 +356,9 @@ describe(NotificationService.name, () => {
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||
album.albumThumbnailAssetId,
|
||||
AssetFileType.Thumbnail,
|
||||
@ -378,7 +390,9 @@ describe(NotificationService.name, () => {
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||
album.albumThumbnailAssetId,
|
||||
AssetFileType.Thumbnail,
|
||||
@ -412,7 +426,9 @@ describe(NotificationService.name, () => {
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([asset.files[0]]);
|
||||
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '', senderName: 'foo' })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||
album.albumThumbnailAssetId,
|
||||
AssetFileType.Thumbnail,
|
||||
@ -434,7 +450,7 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if owner could not be found', async () => {
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create({ ownerId: 'non-existent' })));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.from().owner({ id: 'non-existent' }).build()));
|
||||
|
||||
await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped);
|
||||
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
|
||||
@ -443,7 +459,6 @@ describe(NotificationService.name, () => {
|
||||
it('should skip recipient that could not be looked up', async () => {
|
||||
const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build();
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.user.get.mockResolvedValueOnce(album.owner);
|
||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||
|
||||
@ -226,8 +226,8 @@ export class NotificationService extends BaseService {
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AlbumInvite' })
|
||||
async onAlbumInvite({ id, userId }: ArgOf<'AlbumInvite'>) {
|
||||
await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId } });
|
||||
async onAlbumInvite({ id, userId, senderName }: ArgOf<'AlbumInvite'>) {
|
||||
await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId, senderName } });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'SessionDelete' })
|
||||
@ -303,7 +303,7 @@ export class NotificationService extends BaseService {
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.NotifyAlbumInvite, queue: QueueName.Notification })
|
||||
async handleAlbumInvite({ id, recipientId }: JobOf<JobName.NotifyAlbumInvite>) {
|
||||
async handleAlbumInvite({ id, recipientId, senderName }: JobOf<JobName.NotifyAlbumInvite>) {
|
||||
const album = await this.albumRepository.getById(id, { withAssets: false });
|
||||
if (!album) {
|
||||
return JobStatus.Skipped;
|
||||
@ -314,7 +314,7 @@ export class NotificationService extends BaseService {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumInvite, album.owner.name);
|
||||
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumInvite, senderName);
|
||||
|
||||
const { emailNotifications } = getPreferences(recipient.metadata);
|
||||
|
||||
@ -331,7 +331,7 @@ export class NotificationService extends BaseService {
|
||||
baseUrl: getExternalDomain(server),
|
||||
albumId: album.id,
|
||||
albumName: album.albumName,
|
||||
senderName: album.owner.name,
|
||||
senderName,
|
||||
recipientName: recipient.name,
|
||||
cid: attachment ? attachment.cid : undefined,
|
||||
},
|
||||
@ -360,8 +360,8 @@ export class NotificationService extends BaseService {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
const owner = await this.userRepository.get(album.ownerId, { withDeleted: false });
|
||||
if (!owner) {
|
||||
const recipient = await this.userRepository.get(recipientId, { withDeleted: false });
|
||||
if (!recipient) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
SyncAckDeleteDto,
|
||||
SyncAckSetDto,
|
||||
syncAlbumV2ToV1,
|
||||
syncAssetFaceV2ToV1,
|
||||
SyncAssetV1,
|
||||
SyncItem,
|
||||
@ -60,6 +61,7 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.PartnerStacksV1,
|
||||
SyncRequestType.AlbumAssetsV1,
|
||||
SyncRequestType.AlbumsV1,
|
||||
SyncRequestType.AlbumsV2,
|
||||
SyncRequestType.AlbumUsersV1,
|
||||
SyncRequestType.AlbumToAssetsV1,
|
||||
SyncRequestType.AssetExifsV1,
|
||||
@ -165,6 +167,7 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumsV2]: () => this.syncAlbumsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(options, response, checkpointMap, session.id),
|
||||
@ -412,6 +415,21 @@ export class SyncService extends BaseService {
|
||||
|
||||
const upsertType = SyncEntityType.AlbumV1;
|
||||
const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
const albumUsers = await this.syncRepository.album.getAlbumUsers(data.id);
|
||||
send(response, { type: upsertType, ids: [updateId], data: syncAlbumV2ToV1(data, albumUsers) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumsV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AlbumDeleteV1;
|
||||
const deletes = this.syncRepository.album.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AlbumV2;
|
||||
const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
|
||||
@ -249,6 +249,7 @@ export interface INotifySignupJob extends IEntityJob {
|
||||
|
||||
export interface INotifyAlbumInviteJob extends IEntityJob {
|
||||
recipientId: string;
|
||||
senderName: string;
|
||||
}
|
||||
|
||||
export interface INotifyAlbumUpdateJob extends IEntityJob, IDelayedJob {
|
||||
|
||||
@ -455,3 +455,5 @@ export const updateLockedColumns = <T extends Record<string, unknown> & { locked
|
||||
exif.lockedProperties = lockableProperties.filter((property) => property in exif);
|
||||
return exif;
|
||||
};
|
||||
|
||||
export const dummy = sql`(select 1)`.as('dummy');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { AlbumUserFactory } from 'test/factories/album-user.factory';
|
||||
@ -10,15 +10,12 @@ import { UserFactory } from 'test/factories/user.factory';
|
||||
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
|
||||
|
||||
export class AlbumFactory {
|
||||
#owner: UserFactory;
|
||||
#owner!: UserFactory;
|
||||
#sharedLinks: Selectable<SharedLinkTable>[] = [];
|
||||
#albumUsers: AlbumUserFactory[] = [];
|
||||
#assets: AssetFactory[] = [];
|
||||
|
||||
private constructor(private readonly value: Selectable<AlbumTable>) {
|
||||
value.ownerId ??= newUuid();
|
||||
this.#owner = UserFactory.from({ id: value.ownerId });
|
||||
}
|
||||
private constructor(private readonly value: Selectable<AlbumTable>) {}
|
||||
|
||||
static create(dto: AlbumLike = {}) {
|
||||
return AlbumFactory.from(dto).build();
|
||||
@ -27,7 +24,6 @@ export class AlbumFactory {
|
||||
static from(dto: AlbumLike = {}) {
|
||||
return new AlbumFactory({
|
||||
id: newUuid(),
|
||||
ownerId: newUuid(),
|
||||
albumName: 'My Album',
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: newDate(),
|
||||
@ -43,7 +39,7 @@ export class AlbumFactory {
|
||||
|
||||
owner(dto: UserLike = {}, builder?: FactoryBuilder<UserFactory>) {
|
||||
this.#owner = build(UserFactory.from(dto), builder);
|
||||
this.value.ownerId = this.#owner.build().id;
|
||||
this.albumUser({ userId: this.#owner.build().id, role: AlbumUserRole.Owner });
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -53,7 +49,7 @@ export class AlbumFactory {
|
||||
}
|
||||
|
||||
albumUser(dto: AlbumUserLike = {}, builder?: FactoryBuilder<AlbumUserFactory>) {
|
||||
const albumUser = build(AlbumUserFactory.from(dto).album(this.value), builder);
|
||||
const albumUser = build(AlbumUserFactory.from(dto), builder);
|
||||
this.#albumUsers.push(albumUser);
|
||||
return this;
|
||||
}
|
||||
@ -78,7 +74,6 @@ export class AlbumFactory {
|
||||
build() {
|
||||
return {
|
||||
...this.value,
|
||||
owner: this.#owner.build(),
|
||||
assets: this.#assets.map((asset) => asset.build()),
|
||||
albumUsers: this.#albumUsers.map((albumUser) => albumUser.build()),
|
||||
sharedLinks: this.#sharedLinks,
|
||||
|
||||
@ -84,7 +84,6 @@ export const getForAlbum = (album: ReturnType<AlbumFactory['build']>) => ({
|
||||
createdAt: albumUser.createdAt.toISOString(),
|
||||
user: getDehydrated(albumUser.user),
|
||||
})),
|
||||
owner: getDehydrated(album.owner),
|
||||
sharedLinks: album.sharedLinks.map((sharedLink) => getDehydrated(sharedLink)),
|
||||
});
|
||||
|
||||
@ -219,7 +218,6 @@ export const getForSharedLink = (sharedLink: ReturnType<SharedLinkFactory['build
|
||||
album: sharedLink.album
|
||||
? {
|
||||
...getDehydrated(sharedLink.album),
|
||||
owner: getDehydrated(sharedLink.album.owner),
|
||||
assets: sharedLink.album.assets.map((asset) => getDehydrated(asset)),
|
||||
}
|
||||
: null,
|
||||
|
||||
@ -222,9 +222,14 @@ export class MediumTestContext<S extends BaseService = BaseService> {
|
||||
return { result };
|
||||
}
|
||||
|
||||
async newAlbum(dto: Insertable<AlbumTable>, assetIds?: string[]) {
|
||||
async newAlbum({ ownerId, ...dto }: Insertable<AlbumTable> & { ownerId: string }, assetIds?: string[]) {
|
||||
const album = mediumFactory.albumInsert(dto);
|
||||
const result = await this.get(AlbumRepository).create(album, assetIds ?? [], []);
|
||||
const result = await this.get(AlbumRepository).create(
|
||||
album,
|
||||
assetIds ?? [],
|
||||
[{ userId: ownerId, role: AlbumUserRole.Owner }],
|
||||
ownerId,
|
||||
);
|
||||
return { album, result };
|
||||
}
|
||||
|
||||
@ -570,9 +575,9 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const albumInsert = (album: Partial<Insertable<AlbumTable>> & { ownerId: string }) => {
|
||||
const albumInsert = (album: Partial<Insertable<AlbumTable>>) => {
|
||||
const id = album.id || newUuid();
|
||||
const defaults: Omit<Insertable<AlbumTable>, 'ownerId'> = {
|
||||
const defaults: Insertable<AlbumTable> = {
|
||||
albumName: 'Album',
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,14 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album.id,
|
||||
role: AlbumUserRole.Owner,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@ -47,6 +55,14 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album.id,
|
||||
role: AlbumUserRole.Owner,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@ -136,6 +152,14 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album.id,
|
||||
role: AlbumUserRole.Owner,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@ -163,6 +187,7 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
@ -201,6 +226,7 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumUserV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
@ -229,10 +255,11 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
it('should backfill album users when a user shares an album with you', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user1.id });
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user1.id });
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
// backfill album user
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: user1.id, role: AlbumUserRole.Editor });
|
||||
await wait(2);
|
||||
@ -244,6 +271,15 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album2.id,
|
||||
role: AlbumUserRole.Owner,
|
||||
userId: user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@ -264,6 +300,15 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
// should backfill the album user
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album1.id,
|
||||
role: AlbumUserRole.Owner,
|
||||
userId: user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
|
||||
@ -30,7 +30,6 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: album.id,
|
||||
name: album.albumName,
|
||||
ownerId: album.ownerId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getContextMenuPositionFromEvent, type ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { getShortDateRange } from '$lib/utils/date-time';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -85,12 +85,13 @@
|
||||
{/if}
|
||||
|
||||
{#if showOwner}
|
||||
{#if authManager.user.id === album.ownerId}
|
||||
{@const owner = album.albumUsers[0].user}
|
||||
{#if owner.id === authManager.user.id}
|
||||
<p>{$t('owned')}</p>
|
||||
{:else if album.owner}
|
||||
<p>{$t('shared_by_user', { values: { user: album.owner.name } })}</p>
|
||||
{:else}
|
||||
<p>{$t('shared')}</p>
|
||||
<p>
|
||||
{$t('shared_by_user', { values: { user: owner.name } })}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if album.shared}
|
||||
<p>{$t('shared')}</p>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import { type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { AlbumUserRole, type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
|
||||
import { groupBy } from 'lodash-es';
|
||||
@ -98,24 +98,26 @@
|
||||
/** Group by owner */
|
||||
[AlbumGroupBy.Owner]: (order, albums): AlbumGroup[] => {
|
||||
const currentUserId = authManager.user.id;
|
||||
const groupedByOwnerIds = groupBy(albums, 'ownerId');
|
||||
const groupedByOwnerIds = groupBy(albums, (album) => album.albumUsers[0].user.id);
|
||||
|
||||
const sortSign = order === SortOrder.Desc ? -1 : 1;
|
||||
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerA, albumsA], [ownerB, albumsB]) => {
|
||||
const sortedByOwnerNames = Object.entries(groupedByOwnerIds).sort(([ownerIdA, albumsA], [ownerIdB, albumsB]) => {
|
||||
// We make sure owned albums stay either at the beginning or the end
|
||||
// of the list
|
||||
if (ownerA === currentUserId) {
|
||||
if (ownerIdA === currentUserId) {
|
||||
return -sortSign;
|
||||
} else if (ownerB === currentUserId) {
|
||||
} else if (ownerIdB === currentUserId) {
|
||||
return sortSign;
|
||||
} else {
|
||||
return albumsA[0].owner.name.localeCompare(albumsB[0].owner.name, $locale) * sortSign;
|
||||
const ownerA = albumsA[0].albumUsers[0].user;
|
||||
const ownerB = albumsB[0].albumUsers[0].user;
|
||||
return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign;
|
||||
}
|
||||
});
|
||||
|
||||
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
||||
id: ownerId,
|
||||
name: ownerId === currentUserId ? $t('my_albums') : albums[0].owner.name,
|
||||
name: ownerId === currentUserId ? $t('my_albums') : albums[0].albumUsers[0].user.name,
|
||||
albums,
|
||||
}));
|
||||
},
|
||||
@ -130,7 +132,10 @@
|
||||
return sharedAlbums;
|
||||
}
|
||||
default: {
|
||||
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== authManager.user.id);
|
||||
const nonOwnedAlbums = sharedAlbums.filter(
|
||||
(album) =>
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role !== AlbumUserRole.Owner,
|
||||
);
|
||||
return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
|
||||
}
|
||||
}
|
||||
@ -167,7 +172,9 @@
|
||||
albumGroupIds = groupedAlbums.map(({ id }) => id);
|
||||
});
|
||||
|
||||
let showFullContextMenu = $derived(allowEdit && selectedAlbum && selectedAlbum.ownerId === authManager.user.id);
|
||||
let showFullContextMenu = $derived(
|
||||
allowEdit && selectedAlbum && selectedAlbum.albumUsers[0].user.id === authManager.user.id,
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (allowEdit) {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { AlbumUserRole, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -43,9 +43,11 @@
|
||||
icon={mdiShareVariantOutline}
|
||||
size="16"
|
||||
class="inline ms-1 opacity-70"
|
||||
title={album.ownerId === authManager.user.id
|
||||
title={album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Owner
|
||||
? $t('shared_by_you')
|
||||
: $t('shared_by_user', { values: { user: album.owner.name } })}
|
||||
: $t('shared_by_user', {
|
||||
values: { user: album.albumUsers[0].user.name },
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
import { getAssetType } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum } from '@immich/sdk';
|
||||
import { ReactionType, type ActivityResponseDto, type AlbumUserResponseDto, type AssetTypeEnum } from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, Textarea, toastManager } from '@immich/ui';
|
||||
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiSend, mdiThumbUp } from '@mdi/js';
|
||||
import * as luxon from 'luxon';
|
||||
@ -43,11 +43,11 @@
|
||||
assetId?: string | undefined;
|
||||
albumId: string;
|
||||
assetType?: AssetTypeEnum | undefined;
|
||||
albumOwnerId: string;
|
||||
albumUsers: AlbumUserResponseDto[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
let { assetId = undefined, albumId, assetType = undefined, albumOwnerId, disabled }: Props = $props();
|
||||
let { assetId = undefined, albumId, assetType = undefined, albumUsers, disabled }: Props = $props();
|
||||
|
||||
let innerHeight: number = $state(0);
|
||||
let activityHeight: number = $state(0);
|
||||
@ -56,6 +56,7 @@
|
||||
let previousAssetId: string | undefined = $state(assetId);
|
||||
let message = $state('');
|
||||
let isSendingMessage = $state(false);
|
||||
const isAlbumOwner = $derived(albumUsers[0].user.id === authManager.user.id);
|
||||
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
@ -147,7 +148,7 @@
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
|
||||
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
@ -200,7 +201,7 @@
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === authManager.user.id || albumOwnerId === authManager.user.id}
|
||||
{#if reaction.user.id === authManager.user.id || isAlbumOwner}
|
||||
<div class="me-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
|
||||
@ -638,7 +638,7 @@
|
||||
<ActivityViewer
|
||||
disabled={!album.isActivityEnabled}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumUsers={album.albumUsers}
|
||||
albumId={album.id}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
const isAlbumOwner = $derived(authManager.authenticated && album?.ownerId === authManager.user.id);
|
||||
const isAlbumOwner = $derived(authManager.authenticated && album?.albumUsers[0].user.id === authManager.user.id);
|
||||
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
const { album, onClose }: Props = $props();
|
||||
|
||||
let users: UserResponseDto[] = $state([]);
|
||||
const excludedUserIds = $derived([album.ownerId, ...album.albumUsers.map(({ user: { id } }) => id)]);
|
||||
const excludedUserIds = $derived(album.albumUsers.map(({ user: { id } }) => id));
|
||||
const filteredUsers = $derived(
|
||||
sortBy(
|
||||
users.filter(
|
||||
|
||||
@ -105,16 +105,6 @@
|
||||
<HeaderActionButton action={AddUsers} />
|
||||
</HStack>
|
||||
<div class="ps-2">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</div>
|
||||
<Text class="w-full" size="small">{album.owner.name}</Text>
|
||||
<Field disabled class="w-32 shrink-0">
|
||||
<Select options={[{ label: $t('owner'), value: 'owner' }]} value="owner" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#each album.albumUsers as { user, role } (user.id)}
|
||||
<div class="flex items-center justify-between gap-4 py-2">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
@ -123,12 +113,13 @@
|
||||
</div>
|
||||
<Text size="small">{user.name}</Text>
|
||||
</div>
|
||||
<Field class="w-32">
|
||||
<Field class="w-32" disabled={role === AlbumUserRole.Owner}>
|
||||
<Select
|
||||
value={role}
|
||||
options={[
|
||||
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner },
|
||||
{ label: $t('remove_user'), value: 'none' },
|
||||
] as SelectOption<AlbumUserRole | 'none'>[]}
|
||||
onChange={(value) => handleRoleSelect(user, value)}
|
||||
|
||||
@ -42,7 +42,7 @@ export const getAlbumsActions = ($t: MessageFormatter) => {
|
||||
};
|
||||
|
||||
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) => {
|
||||
const isOwned = authManager.user.id === album.ownerId;
|
||||
const isOwned = album.albumUsers[0].user.id === authManager.user.id;
|
||||
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
|
||||
@ -210,9 +210,7 @@
|
||||
let albumId = $derived(album.id);
|
||||
|
||||
const containsEditors = $derived(album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor));
|
||||
const albumUsers = $derived(
|
||||
showAlbumUsers && containsEditors ? [album.owner, ...album.albumUsers.map(({ user }) => user)] : [],
|
||||
);
|
||||
const albumUsers = $derived(showAlbumUsers && containsEditors ? album.albumUsers.map(({ user }) => user) : []);
|
||||
|
||||
$effect(() => {
|
||||
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
|
||||
@ -231,7 +229,7 @@
|
||||
return { albumId, order: album.order };
|
||||
});
|
||||
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 1);
|
||||
|
||||
$effect(() => {
|
||||
if (assetViewerManager.isViewing || !isShared) {
|
||||
@ -243,16 +241,15 @@
|
||||
|
||||
onDestroy(() => activityManager.reset());
|
||||
|
||||
let isOwned = $derived(authManager.user.id == album.ownerId);
|
||||
const isOwned = $derived(album.albumUsers[0].user.id === authManager.user.id);
|
||||
|
||||
let showActivityStatus = $derived(
|
||||
album.albumUsers.length > 0 &&
|
||||
album.albumUsers.length > 1 &&
|
||||
!assetViewerManager.isViewing &&
|
||||
(album.isActivityEnabled || activityManager.commentCount > 0),
|
||||
);
|
||||
let isEditor = $derived(
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Editor ||
|
||||
album.ownerId === authManager.user.id,
|
||||
const isEditor = $derived(
|
||||
album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Editor || isOwned,
|
||||
);
|
||||
|
||||
let albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer));
|
||||
@ -374,7 +371,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM SHARING -->
|
||||
{#if album.albumUsers.length > 0 || (album.hasSharedLink && isOwned)}
|
||||
{#if album.albumUsers.length > 1 || (album.hasSharedLink && isOwned)}
|
||||
<div class="my-3 flex gap-x-1">
|
||||
<!-- link -->
|
||||
{#if album.hasSharedLink && isOwned}
|
||||
@ -388,13 +385,8 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- owner -->
|
||||
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users with write access (collaborators) -->
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor || role === AlbumUserRole.Owner) as { user } (user.id)}
|
||||
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
@ -620,7 +612,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if album.albumUsers.length > 0 && album && assetViewerManager.isShowActivityPanel && authManager.authenticated && !assetViewerManager.isViewing}
|
||||
{#if album.albumUsers.length > 1 && album && assetViewerManager.isShowActivityPanel && authManager.authenticated && !assetViewerManager.isViewing}
|
||||
<div class="flex">
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
@ -628,7 +620,7 @@
|
||||
class="z-2 w-90 md:w-115 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer disabled={!album.isActivityEnabled} albumOwnerId={album.ownerId} albumId={album.id} />
|
||||
<ActivityViewer disabled={!album.isActivityEnabled} albumUsers={album.albumUsers} albumId={album.id} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { AssetOrder, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
import { userFactory } from './user-factory';
|
||||
|
||||
export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
albumName: Sync.each(() => faker.commerce.product()),
|
||||
@ -11,8 +10,6 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
id: Sync.each(() => faker.string.uuid()),
|
||||
ownerId: Sync.each(() => faker.string.uuid()),
|
||||
owner: userFactory.build(),
|
||||
shared: false,
|
||||
albumUsers: [],
|
||||
hasSharedLink: false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user