mirror of
https://github.com/immich-app/immich.git
synced 2026-05-23 08:02:29 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59384b90b0 |
@@ -3,6 +3,7 @@ import { MapAsset } from 'src/dtos/asset-response.dto';
|
|||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
|
AssetOrder,
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
ChecksumAlgorithm,
|
ChecksumAlgorithm,
|
||||||
@@ -195,6 +196,7 @@ export type SharedLink = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Album = Selectable<AlbumTable> & {
|
export type Album = Selectable<AlbumTable> & {
|
||||||
|
order: AssetOrder;
|
||||||
assets: ShallowDehydrateObject<Selectable<AssetTable>>[];
|
assets: ShallowDehydrateObject<Selectable<AssetTable>>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
@@ -82,6 +82,23 @@ const isAlbumOwned = (ownerId: string) => (eb: ExpressionBuilder<DB, 'album'>) =
|
|||||||
.where('album_user.userId', '=', ownerId),
|
.where('album_user.userId', '=', ownerId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const withOrder = (authUserId?: string) => (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||||
|
const defaultOrder = sql<AssetOrder>`${AssetOrder.Desc}`;
|
||||||
|
|
||||||
|
return authUserId
|
||||||
|
? eb.fn
|
||||||
|
.coalesce(
|
||||||
|
eb
|
||||||
|
.selectFrom('album_user')
|
||||||
|
.select('album_user.order')
|
||||||
|
.whereRef('album_user.albumId', '=', 'album.id')
|
||||||
|
.where('album_user.userId', '=', authUserId),
|
||||||
|
defaultOrder,
|
||||||
|
)
|
||||||
|
.as('order')
|
||||||
|
: defaultOrder.as('order');
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumRepository {
|
export class AlbumRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
@@ -95,6 +112,7 @@ export class AlbumRepository {
|
|||||||
.where('album.id', '=', id)
|
.where('album.id', '=', id)
|
||||||
.where('album.deletedAt', 'is', null)
|
.where('album.deletedAt', 'is', null)
|
||||||
.select(withAlbumUsers(authUserId))
|
.select(withAlbumUsers(authUserId))
|
||||||
|
.select(withOrder(authUserId))
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||||
.$narrowType<{ assets: NotNull }>()
|
.$narrowType<{ assets: NotNull }>()
|
||||||
@@ -118,6 +136,7 @@ export class AlbumRepository {
|
|||||||
.where('album_asset.assetId', '=', assetId)
|
.where('album_asset.assetId', '=', assetId)
|
||||||
.where('album.deletedAt', 'is', null)
|
.where('album.deletedAt', 'is', null)
|
||||||
.select(withAlbumUsers(ownerId))
|
.select(withAlbumUsers(ownerId))
|
||||||
|
.select(withOrder(ownerId))
|
||||||
.orderBy('album.createdAt', 'desc')
|
.orderBy('album.createdAt', 'desc')
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -195,6 +214,7 @@ export class AlbumRepository {
|
|||||||
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner)),
|
.on('album_user.role', '=', sql.lit(AlbumUserRole.Owner)),
|
||||||
)
|
)
|
||||||
.where('album.deletedAt', 'is', null)
|
.where('album.deletedAt', 'is', null)
|
||||||
|
.select('album_user.order as order')
|
||||||
.select(withAlbumUsers(ownerId))
|
.select(withAlbumUsers(ownerId))
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.orderBy('album.createdAt', 'desc')
|
.orderBy('album.createdAt', 'desc')
|
||||||
@@ -239,6 +259,7 @@ export class AlbumRepository {
|
|||||||
)
|
)
|
||||||
.where('album.deletedAt', 'is', null)
|
.where('album.deletedAt', 'is', null)
|
||||||
.select(withAlbumUsers(ownerId))
|
.select(withAlbumUsers(ownerId))
|
||||||
|
.select(withOrder(ownerId))
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.orderBy('album.createdAt', 'desc')
|
.orderBy('album.createdAt', 'desc')
|
||||||
.execute();
|
.execute();
|
||||||
@@ -271,6 +292,7 @@ export class AlbumRepository {
|
|||||||
.where(({ not, exists, selectFrom }) =>
|
.where(({ not, exists, selectFrom }) =>
|
||||||
not(exists(selectFrom('shared_link').whereRef('shared_link.albumId', '=', 'album.id'))),
|
not(exists(selectFrom('shared_link').whereRef('shared_link.albumId', '=', 'album.id'))),
|
||||||
)
|
)
|
||||||
|
.select('album_user.order as order')
|
||||||
.select(withSharedLink)
|
.select(withSharedLink)
|
||||||
.select(withAlbumUsers(ownerId))
|
.select(withAlbumUsers(ownerId))
|
||||||
.orderBy('album.createdAt', 'desc')
|
.orderBy('album.createdAt', 'desc')
|
||||||
@@ -350,13 +372,13 @@ export class AlbumRepository {
|
|||||||
params: [
|
params: [
|
||||||
{ albumName: DummyValue.STRING },
|
{ albumName: DummyValue.STRING },
|
||||||
[],
|
[],
|
||||||
[{ userId: DummyValue.UUID, role: AlbumUserRole.Owner }, DummyValue.UUID],
|
[{ userId: DummyValue.UUID, role: AlbumUserRole.Owner, order: AssetOrder.Desc }, DummyValue.UUID],
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async create(
|
async create(
|
||||||
album: Insertable<AlbumTable>,
|
album: Insertable<AlbumTable>,
|
||||||
assetIds: string[],
|
assetIds: string[],
|
||||||
albumUsers: AlbumUserCreateDto[],
|
albumUsers: (AlbumUserCreateDto & { order?: AssetOrder })[],
|
||||||
authUserId: string,
|
authUserId: string,
|
||||||
) {
|
) {
|
||||||
if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) {
|
if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) {
|
||||||
@@ -365,12 +387,14 @@ export class AlbumRepository {
|
|||||||
|
|
||||||
const userIds = albumUsers.map((u) => u.userId);
|
const userIds = albumUsers.map((u) => u.userId);
|
||||||
const roles = albumUsers.map((u) => u.role);
|
const roles = albumUsers.map((u) => u.role);
|
||||||
|
const orders = albumUsers.map((u) => u.order ?? AssetOrder.Desc);
|
||||||
|
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.with('album', (db) => db.insertInto('album').values(album).returningAll())
|
.with('album', (db) => db.insertInto('album').values(album).returningAll())
|
||||||
.with('album_user', (db) =>
|
.with('album_user', (db) =>
|
||||||
db
|
db
|
||||||
.insertInto('album_user')
|
.insertInto('album_user')
|
||||||
|
.columns(['albumId', 'userId', 'role', 'order'])
|
||||||
.expression((eb) =>
|
.expression((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('album')
|
.selectFrom('album')
|
||||||
@@ -378,13 +402,15 @@ export class AlbumRepository {
|
|||||||
ref('album.id').as('albumId'),
|
ref('album.id').as('albumId'),
|
||||||
sql`unnest(${userIds}::uuid[])`.as('userId'),
|
sql`unnest(${userIds}::uuid[])`.as('userId'),
|
||||||
sql`unnest(${roles}::album_user_role_enum[])`.as('role'),
|
sql`unnest(${roles}::album_user_role_enum[])`.as('role'),
|
||||||
|
sql`unnest(${orders}::varchar[])`.as('order'),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.returning(['album_user.albumId', 'album_user.userId', 'album_user.role']),
|
.returning(['album_user.albumId', 'album_user.userId', 'album_user.role', 'album_user.order']),
|
||||||
)
|
)
|
||||||
.with('album_asset', (db) =>
|
.with('album_asset', (db) =>
|
||||||
db
|
db
|
||||||
.insertInto('album_asset')
|
.insertInto('album_asset')
|
||||||
|
.columns(['albumId', 'assetId'])
|
||||||
.expression((eb) =>
|
.expression((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('album')
|
.selectFrom('album')
|
||||||
@@ -396,6 +422,7 @@ export class AlbumRepository {
|
|||||||
.selectFrom('album')
|
.selectFrom('album')
|
||||||
.selectAll('album')
|
.selectAll('album')
|
||||||
.select(withAlbumUsers(authUserId))
|
.select(withAlbumUsers(authUserId))
|
||||||
|
.select(withOrder(authUserId))
|
||||||
.select(withAssets)
|
.select(withAssets)
|
||||||
.$narrowType<{ assets: NotNull }>()
|
.$narrowType<{ assets: NotNull }>()
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
@@ -403,15 +430,27 @@ export class AlbumRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, album: Updateable<AlbumTable>, authUserId: string) {
|
update(id: string, album: Updateable<AlbumTable>, authUserId: string, order?: AssetOrder) {
|
||||||
return this.db
|
return this.db.transaction().execute(async (db) => {
|
||||||
.updateTable('album')
|
if (order !== undefined) {
|
||||||
.set(album)
|
await db
|
||||||
.where('album.id', '=', id)
|
.updateTable('album_user')
|
||||||
.returningAll('album')
|
.set({ order })
|
||||||
.returning(withSharedLink)
|
.where('albumId', '=', id)
|
||||||
.returning(withAlbumUsers(authUserId))
|
.where('userId', '=', authUserId)
|
||||||
.executeTakeFirstOrThrow();
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
.updateTable('album')
|
||||||
|
.set(album)
|
||||||
|
.where('album.id', '=', id)
|
||||||
|
.returningAll('album')
|
||||||
|
.returning(withSharedLink)
|
||||||
|
.returning(withAlbumUsers(authUserId))
|
||||||
|
.returning(withOrder(authUserId))
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import _ from 'lodash';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { Album, columns } from 'src/database';
|
import { Album, columns } from 'src/database';
|
||||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserRole, SharedLinkType } from 'src/enum';
|
import { AlbumUserRole, AssetOrder, SharedLinkType } from 'src/enum';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
@@ -55,7 +55,15 @@ const withAlbumOwner = (eb: ExpressionBuilder<DB, 'album'>) => {
|
|||||||
const withSharedLinkAlbum = (eb: ExpressionBuilder<DB, 'shared_link'>) => {
|
const withSharedLinkAlbum = (eb: ExpressionBuilder<DB, 'shared_link'>) => {
|
||||||
return eb
|
return eb
|
||||||
.selectFrom('album')
|
.selectFrom('album')
|
||||||
|
.leftJoin('album_user as album_order', (join) =>
|
||||||
|
join
|
||||||
|
.onRef('album_order.albumId', '=', 'album.id')
|
||||||
|
.onRef('album_order.userId', '=', 'shared_link.userId'),
|
||||||
|
)
|
||||||
.selectAll('album')
|
.selectAll('album')
|
||||||
|
.select((eb) =>
|
||||||
|
eb.fn.coalesce('album_order.order', sql<AssetOrder>`${AssetOrder.Desc}`).as('order'),
|
||||||
|
)
|
||||||
.whereRef('album.id', '=', 'shared_link.albumId')
|
.whereRef('album.id', '=', 'shared_link.albumId')
|
||||||
.where('album.deletedAt', 'is', null);
|
.where('album.deletedAt', 'is', null);
|
||||||
};
|
};
|
||||||
@@ -107,7 +115,7 @@ export class SharedLinkRepository {
|
|||||||
.as('assets'),
|
.as('assets'),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson('owner').as('owner'))
|
.select((eb) => eb.fn.toJson('owner').as('owner'))
|
||||||
.groupBy(['album.id', sql`"owner".*`])
|
.groupBy(['album.id', 'album_order.order', sql`"owner".*`])
|
||||||
.as('album'),
|
.as('album'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class AlbumSync extends BaseSync {
|
|||||||
const userId = options.userId;
|
const userId = options.userId;
|
||||||
return this.upsertQuery('album', options)
|
return this.upsertQuery('album', options)
|
||||||
.distinctOn(['album.id', 'album.updateId'])
|
.distinctOn(['album.id', 'album.updateId'])
|
||||||
.leftJoin('album_user as album_users', 'album.id', 'album_users.albumId')
|
.innerJoin('album_user as album_users', 'album.id', 'album_users.albumId')
|
||||||
.where('album_users.userId', '=', userId)
|
.where('album_users.userId', '=', userId)
|
||||||
.select([
|
.select([
|
||||||
'album.id',
|
'album.id',
|
||||||
@@ -180,7 +180,7 @@ class AlbumSync extends BaseSync {
|
|||||||
'album.updatedAt',
|
'album.updatedAt',
|
||||||
'album.albumThumbnailAssetId as thumbnailAssetId',
|
'album.albumThumbnailAssetId as thumbnailAssetId',
|
||||||
'album.isActivityEnabled',
|
'album.isActivityEnabled',
|
||||||
'album.order',
|
'album_users.order as order',
|
||||||
'album.updateId',
|
'album.updateId',
|
||||||
])
|
])
|
||||||
.stream();
|
.stream();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "album_user" ADD "order" character varying NOT NULL DEFAULT 'desc';`.execute(db);
|
||||||
|
await sql`UPDATE "album_user" SET "order" = "album"."order" FROM "album" WHERE "album_user"."albumId" = "album"."id";`.execute(
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
await sql`ALTER TABLE "album" DROP COLUMN "order";`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "album" ADD "order" character varying NOT NULL DEFAULT 'desc';`.execute(db);
|
||||||
|
await sql`
|
||||||
|
UPDATE "album"
|
||||||
|
SET "order" = "album_user"."order"
|
||||||
|
FROM "album_user"
|
||||||
|
WHERE "album_user"."albumId" = "album"."id"
|
||||||
|
AND "album_user"."role" = 'owner';
|
||||||
|
`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_user" DROP COLUMN "order";`.execute(db);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||||
import { album_user_role_enum } from 'src/schema/enums';
|
import { album_user_role_enum } from 'src/schema/enums';
|
||||||
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
@@ -58,6 +58,9 @@ export class AlbumUserTable {
|
|||||||
@Column({ enum: album_user_role_enum, default: AlbumUserRole.Editor })
|
@Column({ enum: album_user_role_enum, default: AlbumUserRole.Editor })
|
||||||
role!: Generated<AlbumUserRole>;
|
role!: Generated<AlbumUserRole>;
|
||||||
|
|
||||||
|
@Column({ default: AssetOrder.Desc })
|
||||||
|
order!: Generated<AssetOrder>;
|
||||||
|
|
||||||
@CreateIdColumn({ index: true })
|
@CreateIdColumn({ index: true })
|
||||||
createId!: Generated<string>;
|
createId!: Generated<string>;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetOrder } from 'src/enum';
|
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
|
|
||||||
@Table({ name: 'album' })
|
@Table({ name: 'album' })
|
||||||
@@ -45,9 +44,6 @@ export class AlbumTable {
|
|||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
isActivityEnabled!: Generated<boolean>;
|
isActivityEnabled!: Generated<boolean>;
|
||||||
|
|
||||||
@Column({ default: AssetOrder.Desc })
|
|
||||||
order!: Generated<AssetOrder>;
|
|
||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,19 +182,19 @@ describe(AlbumService.name, () => {
|
|||||||
{
|
{
|
||||||
albumName: 'test',
|
albumName: 'test',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
order: album.order,
|
|
||||||
albumThumbnailAssetId: assetId,
|
albumThumbnailAssetId: assetId,
|
||||||
},
|
},
|
||||||
[assetId],
|
[assetId],
|
||||||
[
|
[
|
||||||
{ userId: owner.id, role: AlbumUserRole.Owner },
|
{ userId: owner.id, role: AlbumUserRole.Owner, order: AssetOrder.Desc },
|
||||||
{ userId: albumUser.userId, role: AlbumUserRole.Editor },
|
{ userId: albumUser.userId, role: AlbumUserRole.Editor, order: AssetOrder.Desc },
|
||||||
],
|
],
|
||||||
owner.id,
|
owner.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
||||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
||||||
|
expect(mocks.user.getMetadata).toHaveBeenCalledWith(albumUser.userId);
|
||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledTimes(1);
|
expect(mocks.event.emit).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||||
@@ -238,16 +238,19 @@ describe(AlbumService.name, () => {
|
|||||||
{
|
{
|
||||||
albumName: album.albumName,
|
albumName: album.albumName,
|
||||||
description: album.description,
|
description: album.description,
|
||||||
order: 'asc',
|
|
||||||
albumThumbnailAssetId: assetId,
|
albumThumbnailAssetId: assetId,
|
||||||
},
|
},
|
||||||
[assetId],
|
[assetId],
|
||||||
[{ userId: owner.id, role: AlbumUserRole.Owner }, albumUser],
|
[
|
||||||
|
{ userId: owner.id, role: AlbumUserRole.Owner, order: 'asc' },
|
||||||
|
{ ...albumUser, order: 'asc' },
|
||||||
|
],
|
||||||
owner.id,
|
owner.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
|
||||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
|
||||||
|
expect(mocks.user.getMetadata).toHaveBeenCalledWith(albumUser.userId);
|
||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||||
id: album.id,
|
id: album.id,
|
||||||
@@ -290,11 +293,10 @@ describe(AlbumService.name, () => {
|
|||||||
{
|
{
|
||||||
albumName: album.albumName,
|
albumName: album.albumName,
|
||||||
description: album.description,
|
description: album.description,
|
||||||
order: 'desc',
|
|
||||||
albumThumbnailAssetId: assetId,
|
albumThumbnailAssetId: assetId,
|
||||||
},
|
},
|
||||||
[assetId],
|
[assetId],
|
||||||
[{ userId: owner.id, role: AlbumUserRole.Owner }],
|
[{ userId: owner.id, role: AlbumUserRole.Owner, order: 'desc' }],
|
||||||
owner.id,
|
owner.id,
|
||||||
);
|
);
|
||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId, 'asset-2']), false);
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId, 'asset-2']), false);
|
||||||
@@ -364,10 +366,24 @@ describe(AlbumService.name, () => {
|
|||||||
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.album.update).toHaveBeenCalledWith(
|
expect(mocks.album.update).toHaveBeenCalledWith(
|
||||||
album.id,
|
album.id,
|
||||||
{ id: album.id, albumName: 'new album name' },
|
expect.objectContaining({ id: album.id, albumName: 'new album name' }),
|
||||||
owner.id,
|
owner.id,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the album order for the auth user', async () => {
|
||||||
|
const album = AlbumFactory.create({ order: AssetOrder.Desc });
|
||||||
|
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, order: AssetOrder.Asc }));
|
||||||
|
|
||||||
|
await sut.update(AuthFactory.create(owner), album.id, { order: AssetOrder.Asc });
|
||||||
|
|
||||||
|
expect(mocks.album.update).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.album.update).toHaveBeenCalledWith(album.id, { id: album.id }, owner.id, AssetOrder.Asc);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
@@ -464,6 +480,7 @@ describe(AlbumService.name, () => {
|
|||||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
mocks.album.update.mockResolvedValue(getForAlbum(album));
|
||||||
mocks.user.get.mockResolvedValue(user);
|
mocks.user.get.mockResolvedValue(user);
|
||||||
|
mocks.user.getMetadata.mockResolvedValue([]);
|
||||||
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
|
||||||
|
|
||||||
await sut.addUsers(AuthFactory.create(owner), album.id, { albumUsers: [{ userId: user.id }] });
|
await sut.addUsers(AuthFactory.create(owner), album.id, { albumUsers: [{ userId: user.id }] });
|
||||||
@@ -471,7 +488,9 @@ describe(AlbumService.name, () => {
|
|||||||
expect(mocks.albumUser.create).toHaveBeenCalledWith({
|
expect(mocks.albumUser.create).toHaveBeenCalledWith({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
|
order: AssetOrder.Desc,
|
||||||
});
|
});
|
||||||
|
expect(mocks.user.getMetadata).toHaveBeenCalledWith(user.id);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||||
id: album.id,
|
id: album.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -124,16 +124,22 @@ export class AlbumService extends BaseService {
|
|||||||
const assetIds = [...allowedAssetIdsSet].map((id) => id);
|
const assetIds = [...allowedAssetIdsSet].map((id) => id);
|
||||||
|
|
||||||
const userMetadata = await this.userRepository.getMetadata(auth.user.id);
|
const userMetadata = await this.userRepository.getMetadata(auth.user.id);
|
||||||
|
const ownerOrder = getPreferences(userMetadata).albums.defaultAssetOrder;
|
||||||
|
const albumUsersWithOrder = await Promise.all(
|
||||||
|
albumUsers.map(async (albumUser) => {
|
||||||
|
const userMetadata = await this.userRepository.getMetadata(albumUser.userId);
|
||||||
|
return { ...albumUser, order: getPreferences(userMetadata).albums.defaultAssetOrder };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const album = await this.albumRepository.create(
|
const album = await this.albumRepository.create(
|
||||||
{
|
{
|
||||||
albumName: dto.albumName,
|
albumName: dto.albumName,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
albumThumbnailAssetId: assetIds[0] || null,
|
albumThumbnailAssetId: assetIds[0] || null,
|
||||||
order: getPreferences(userMetadata).albums.defaultAssetOrder,
|
|
||||||
},
|
},
|
||||||
assetIds,
|
assetIds,
|
||||||
[{ userId: auth.user.id, role: AlbumUserRole.Owner }, ...albumUsers],
|
[{ userId: auth.user.id, role: AlbumUserRole.Owner, order: ownerOrder }, ...albumUsersWithOrder],
|
||||||
auth.user.id,
|
auth.user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -155,6 +161,7 @@ export class AlbumService extends BaseService {
|
|||||||
throw new BadRequestException('Invalid album thumbnail');
|
throw new BadRequestException('Invalid album thumbnail');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedAlbum = await this.albumRepository.update(
|
const updatedAlbum = await this.albumRepository.update(
|
||||||
album.id,
|
album.id,
|
||||||
{
|
{
|
||||||
@@ -163,9 +170,9 @@ export class AlbumService extends BaseService {
|
|||||||
description: dto.description,
|
description: dto.description,
|
||||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||||
isActivityEnabled: dto.isActivityEnabled,
|
isActivityEnabled: dto.isActivityEnabled,
|
||||||
order: dto.order,
|
|
||||||
},
|
},
|
||||||
auth.user.id,
|
auth.user.id,
|
||||||
|
dto.order,
|
||||||
);
|
);
|
||||||
|
|
||||||
return mapAlbum({ ...updatedAlbum, assets: album.assets });
|
return mapAlbum({ ...updatedAlbum, assets: album.assets });
|
||||||
@@ -307,7 +314,10 @@ export class AlbumService extends BaseService {
|
|||||||
throw new BadRequestException('Invalid user');
|
throw new BadRequestException('Invalid user');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.albumUserRepository.create({ userId, albumId: id, role });
|
const userMetadata = await this.userRepository.getMetadata(userId);
|
||||||
|
const order = getPreferences(userMetadata).albums.defaultAssetOrder;
|
||||||
|
|
||||||
|
await this.albumUserRepository.create({ userId, albumId: id, role, order });
|
||||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||||
import { AlbumFactory } from 'test/factories/album.factory';
|
import { AlbumFactory } from 'test/factories/album.factory';
|
||||||
import { build } from 'test/factories/builder.factory';
|
import { build } from 'test/factories/builder.factory';
|
||||||
@@ -24,6 +24,7 @@ export class AlbumUserFactory {
|
|||||||
albumId: newUuid(),
|
albumId: newUuid(),
|
||||||
userId: newUuid(),
|
userId: newUuid(),
|
||||||
role: AlbumUserRole.Editor,
|
role: AlbumUserRole.Editor,
|
||||||
|
order: AssetOrder.Desc,
|
||||||
createId: newUuidV7(),
|
createId: newUuidV7(),
|
||||||
createdAt: newDate(),
|
createdAt: newDate(),
|
||||||
updateId: newUuidV7(),
|
updateId: newUuidV7(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class AlbumFactory {
|
|||||||
#albumUsers: AlbumUserFactory[] = [];
|
#albumUsers: AlbumUserFactory[] = [];
|
||||||
#assets: AssetFactory[] = [];
|
#assets: AssetFactory[] = [];
|
||||||
|
|
||||||
private constructor(private readonly value: Selectable<AlbumTable>) {}
|
private constructor(private readonly value: Selectable<AlbumTable> & { order: AssetOrder }) {}
|
||||||
|
|
||||||
static create(dto: AlbumLike = {}) {
|
static create(dto: AlbumLike = {}) {
|
||||||
return AlbumFactory.from(dto).build();
|
return AlbumFactory.from(dto).build();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
|
import { AssetOrder } from 'src/enum';
|
||||||
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
||||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||||
@@ -23,7 +24,7 @@ export type AssetLike = Partial<Selectable<AssetTable>>;
|
|||||||
export type AssetExifLike = Partial<Selectable<AssetExifTable>>;
|
export type AssetExifLike = Partial<Selectable<AssetExifTable>>;
|
||||||
export type AssetEditLike = Partial<Selectable<AssetEditTable>>;
|
export type AssetEditLike = Partial<Selectable<AssetEditTable>>;
|
||||||
export type AssetFileLike = Partial<Selectable<AssetFileTable>>;
|
export type AssetFileLike = Partial<Selectable<AssetFileTable>>;
|
||||||
export type AlbumLike = Partial<Selectable<AlbumTable>>;
|
export type AlbumLike = Partial<Selectable<AlbumTable> & { order: AssetOrder }>;
|
||||||
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
|
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
|
||||||
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
|
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
|
||||||
export type UserLike = Partial<Selectable<UserTable>>;
|
export type UserLike = Partial<Selectable<UserTable>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user