Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: API and UI to replace asset data without changing record id #8480

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
97e108b
Add api to update existing asset
midzelis Apr 3, 2024
2a09ff6
Merge branch 'main' of github.com:immich-app/immich into pr/midzelis/…
alextran1502 Apr 10, 2024
f78d271
Finish impl, write tests
midzelis Apr 18, 2024
6d6d837
Merge branch 'main' into update_asset
midzelis Apr 18, 2024
f502ea9
Hook up replace asset backend to web frontend
midzelis Apr 18, 2024
01564a7
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis Apr 18, 2024
a3186cb
Fix bad merge/failing test
midzelis Apr 18, 2024
3cda7ea
Build fixes
midzelis Apr 18, 2024
ba325dd
Remove c param from api
midzelis Apr 18, 2024
aba5ccf
OpenAPI and style
midzelis Apr 18, 2024
6cfef44
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 7, 2024
e3da392
Tests, API gen
midzelis May 7, 2024
0117266
Create common update dto
midzelis May 7, 2024
675f770
Check access. Rename clone to copy. Change exception kind
midzelis May 9, 2024
fa630b6
Simplify, handle revert on duplicate
midzelis May 9, 2024
07b014d
Upload backend refactor/simplication for asset update
midzelis May 11, 2024
9a4a8f6
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 11, 2024
bb457d2
Run open-api gen
midzelis May 11, 2024
5bc5d36
Lost in the merge
midzelis May 11, 2024
422b4c9
formating... :-(
midzelis May 11, 2024
ae3f9b1
chore: clean up
jrasm91 May 16, 2024
453a502
refactor: rename endpoints
jrasm91 May 16, 2024
0b17c63
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 17, 2024
85ac832
Fix merge
midzelis May 17, 2024
2b54b00
Fix up tests for AssetMediaUploadResponseDto
midzelis May 18, 2024
a1bc339
Update tests to new endpoints
midzelis May 18, 2024
49a3333
Update and create new e2e tests
midzelis May 20, 2024
f812a53
Accidental file
midzelis May 20, 2024
79cc441
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 20, 2024
e42dfea
lint - fix duplicateId query
midzelis May 20, 2024
0cbe10f
Fix up web uploader for new responses
midzelis May 20, 2024
d63dfac
typo
midzelis May 20, 2024
6209de7
fix test
midzelis May 20, 2024
8ac06d9
fix web test
midzelis May 20, 2024
01cc652
Make return type more consistent, status code dependent
midzelis May 21, 2024
c919c1a
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 21, 2024
bf24d06
format/lint
midzelis May 21, 2024
8723144
lint
midzelis May 21, 2024
c57af80
Rerun ci - empty
midzelis May 21, 2024
c07eb45
Shuffle test order
midzelis May 21, 2024
d8f480a
Rename responses, no past tense, fix tests
midzelis May 21, 2024
10b0359
Merge remote-tracking branch 'upstream/main' into update_asset
midzelis May 21, 2024
def3d3f
Remove doc
midzelis May 21, 2024
fa5d8d7
remove libraryId
midzelis May 21, 2024
11e6569
Fix test
midzelis May 21, 2024
a3162eb
unused var/import
midzelis May 21, 2024
4c57538
Increase beforeAll timeout
midzelis May 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 2 additions & 7 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
"editor.tabSize": 2
},
"svelte.enable-ts-plugin": true,
"eslint.validate": [
"javascript",
"svelte"
],
"eslint.validate": ["javascript", "svelte"],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[dart]": {
"editor.formatOnSave": true,
Expand All @@ -34,9 +31,7 @@
"editor.wordBasedSuggestions": "off",
"editor.defaultFormatter": "Dart-Code.dart-code"
},
"cSpell.words": [
"immich"
],
"cSpell.words": ["immich"],
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
Expand Down
1 change: 1 addition & 0 deletions e2e/dist/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 16 additions & 14 deletions e2e/src/api/specs/activity.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
ActivityCreateDto,
AlbumResponseDto,
AlbumUserRole,
AssetFileUploadResponseDto,
AssetMediaCreateResponseDto,
LoginResponseDto,
ReactionType,
createActivity as create,
Expand All @@ -17,7 +17,8 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => {
let admin: LoginResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetFileUploadResponseDto;

let assetId: string;
let album: AlbumResponseDto;

const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
Expand All @@ -28,12 +29,13 @@ describe('/activity', () => {

admin = await utils.adminSetup();
nonOwner = await utils.userSetup(admin.accessToken, createUserDto.user1);
asset = await utils.createAsset(admin.accessToken);
const assetMediaResponse = (await utils.createAsset(admin.accessToken)) as AssetMediaCreateResponseDto;
assetId = assetMediaResponse.assetId;
album = await createAlbum(
{
createAlbumDto: {
albumName: 'Album 1',
assetIds: [asset.id],
assetIds: [assetId],
albumUsers: [{ userId: nonOwner.userId, role: AlbumUserRole.Editor }],
},
},
Expand Down Expand Up @@ -90,7 +92,7 @@ describe('/activity', () => {
{
createAlbumDto: {
albumName: 'Album 2',
assetIds: [asset.id],
assetIds: [assetId],
},
},
{ headers: asBearerAuth(admin.accessToken) },
Expand Down Expand Up @@ -173,15 +175,15 @@ describe('/activity', () => {
const [reaction] = await Promise.all([
createActivity({
albumId: album.id,
assetId: asset.id,
assetId: assetId,
type: ReactionType.Like,
}),
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);

const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id, assetId: asset.id })
.query({ albumId: album.id, assetId: assetId })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body.length).toBe(1);
Expand Down Expand Up @@ -263,7 +265,7 @@ describe('/activity', () => {
it('should not confuse an album like with an asset like', async () => {
const reaction = await createActivity({
albumId: album.id,
assetId: asset.id,
assetId: assetId,
type: ReactionType.Like,
});
const { status, body } = await request(app)
Expand All @@ -280,14 +282,14 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
albumId: album.id,
assetId: asset.id,
assetId: assetId,
type: 'comment',
comment: 'This is my first comment',
});
expect(status).toEqual(201);
expect(body).toEqual({
id: expect.any(String),
assetId: asset.id,
assetId: assetId,
createdAt: expect.any(String),
type: 'comment',
comment: 'This is my first comment',
Expand All @@ -299,11 +301,11 @@ describe('/activity', () => {
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
.send({ albumId: album.id, assetId: assetId, type: 'like' });
expect(status).toEqual(201);
expect(body).toEqual({
id: expect.any(String),
assetId: asset.id,
assetId: assetId,
createdAt: expect.any(String),
type: 'like',
comment: null,
Expand All @@ -314,14 +316,14 @@ describe('/activity', () => {
it('should return a 200 for a duplicate like on an asset', async () => {
const reaction = await createActivity({
albumId: album.id,
assetId: asset.id,
assetId: assetId,
type: ReactionType.Like,
});

const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
.send({ albumId: album.id, assetId: assetId, type: 'like' });
expect(status).toEqual(200);
expect(body).toEqual(reaction);
});
Expand Down
75 changes: 38 additions & 37 deletions e2e/src/api/specs/album.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
addAssetsToAlbum,
AlbumResponseDto,
AlbumUserRole,
AssetFileUploadResponseDto,
AssetMediaCreateResponseDto,
AssetOrder,
deleteUser,
getAlbumInfo,
Expand All @@ -26,8 +26,8 @@ const user2NotShared = 'user2NotShared';
describe('/album', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetFileUploadResponseDto;
let user1Asset2: AssetFileUploadResponseDto;
let user1Asset1: string;
let user1Asset2: string;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];
Expand All @@ -44,29 +44,30 @@ describe('/album', () => {
utils.userSetup(admin.accessToken, createUserDto.user3),
]);

[user1Asset1, user1Asset2] = await Promise.all([
const responses = await Promise.all([
utils.createAsset(user1.accessToken, { isFavorite: true }),
utils.createAsset(user1.accessToken),
]);
[user1Asset1, user1Asset2] = responses.map((response) => (response as AssetMediaCreateResponseDto).assetId!);

user1Albums = await Promise.all([
utils.createAlbum(user1.accessToken, {
albumName: user1SharedEditorUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
assetIds: [user1Asset1.id],
assetIds: [user1Asset1],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
assetIds: [user1Asset1],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
assetIds: [user1Asset1, user1Asset2],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedViewerUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
assetIds: [user1Asset1.id],
assetIds: [user1Asset1],
}),
]);

Expand All @@ -88,7 +89,7 @@ describe('/album', () => {
});

await addAssetsToAlbum(
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1] } },
{ headers: asBearerAuth(user1.accessToken) },
);

Expand Down Expand Up @@ -258,23 +259,23 @@ describe('/album', () => {

it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/album?assetId=${user1Asset2.id}`)
.get(`/album?assetId=${user1Asset2}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
});

it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(app)
.get(`/album?shared=true&assetId=${user1Asset1.id}`)
.get(`/album?shared=true&assetId=${user1Asset1}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});

it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(app)
.get(`/album?shared=false&assetId=${user1Asset1.id}`)
.get(`/album?shared=false&assetId=${user1Asset1}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
Expand Down Expand Up @@ -403,49 +404,49 @@ describe('/album', () => {
});

it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { assetId } = (await utils.createAsset(user1.accessToken)) as AssetMediaCreateResponseDto;
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
.send({ ids: [assetId] });

expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect(body).toEqual([expect.objectContaining({ id: assetId, success: true })]);
});

it('should be able to add own asset to shared album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { assetId } = (await utils.createAsset(user1.accessToken)) as AssetMediaCreateResponseDto;
const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
.send({ ids: [assetId] });

expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect(body).toEqual([expect.objectContaining({ id: assetId, success: true })]);
});

it('should not be able to add assets to album as a viewer', async () => {
const asset = await utils.createAsset(user2.accessToken);
const { assetId } = (await utils.createAsset(user2.accessToken)) as AssetMediaCreateResponseDto;
const { status, body } = await request(app)
.put(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [asset.id] });
.send({ ids: [assetId] });

expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access'));
});

it('should add duplicate assets only once', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { assetId } = (await utils.createAsset(user1.accessToken)) as AssetMediaCreateResponseDto;
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id, asset.id] });
.send({ ids: [assetId, assetId] });

expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
expect.objectContaining({ id: asset.id, success: false, error: 'duplicate' }),
expect.objectContaining({ id: assetId, success: true }),
expect.objectContaining({ id: assetId, success: false, error: 'duplicate' }),
]);
});
});
Expand Down Expand Up @@ -504,7 +505,7 @@ describe('/album', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
Expand All @@ -514,12 +515,12 @@ describe('/album', () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
id: user1Asset1,
success: false,
error: 'no_permission',
}),
Expand All @@ -530,12 +531,12 @@ describe('/album', () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
id: user1Asset1,
success: false,
error: 'no_permission',
}),
Expand All @@ -546,27 +547,27 @@ describe('/album', () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1, success: true })]);
});

it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1, success: true })]);
});

it('should not be able to remove assets from album as a viewer', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset1] });

expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access'));
Expand All @@ -576,12 +577,12 @@ describe('/album', () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id, user1Asset1.id] });
.send({ ids: [user1Asset1, user1Asset1] });

expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
expect.objectContaining({ id: user1Asset1.id, success: false, error: 'not_found' }),
expect.objectContaining({ id: user1Asset1, success: true }),
expect.objectContaining({ id: user1Asset1, success: false, error: 'not_found' }),
]);
});
});
Expand Down