1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-10 05:46:46 +01:00
immich/server/src/services/asset-v1.service.spec.ts

221 lines
8.7 KiB
TypeScript
Raw Normal View History

import { when } from 'jest-when';
2024-03-21 14:07:47 +01:00
import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
2024-03-20 22:02:51 +01:00
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
2024-03-21 14:07:47 +01:00
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
2024-03-21 14:07:47 +01:00
import { AssetServiceV1 } from 'src/services/asset-v1.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { QueryFailedError } from 'typeorm';
const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
createAssetDto.deviceAssetId = 'deviceAssetId';
createAssetDto.deviceId = 'deviceId';
createAssetDto.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
createAssetDto.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
createAssetDto.isFavorite = false;
createAssetDto.isArchived = false;
createAssetDto.duration = '0:00:00.000000';
createAssetDto.libraryId = 'libraryId';
return createAssetDto;
};
const _getAsset_1 = () => {
const asset_1 = new AssetEntity();
asset_1.id = 'id_1';
asset_1.ownerId = 'user_id_1';
asset_1.deviceAssetId = 'device_asset_id_1';
asset_1.deviceId = 'device_id_1';
asset_1.type = AssetType.VIDEO;
asset_1.originalPath = 'fake_path/asset_1.jpeg';
asset_1.resizePath = '';
asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z');
asset_1.isFavorite = false;
asset_1.isArchived = false;
asset_1.webpPath = '';
asset_1.encodedVideoPath = '';
asset_1.duration = '0:00:00.000000';
asset_1.exifInfo = new ExifEntity();
asset_1.exifInfo.latitude = 49.533_547;
asset_1.exifInfo.longitude = 10.703_075;
return asset_1;
};
describe('AssetService', () => {
2024-03-21 14:07:47 +01:00
let sut: AssetServiceV1;
let accessMock: IAccessRepositoryMock;
let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
let assetMock: jest.Mocked<IAssetRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let libraryMock: jest.Mocked<ILibraryRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => {
assetRepositoryMockV1 = {
get: jest.fn(),
getAllByUserId: jest.fn(),
getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
feat(server): Add support for client-side hashing (#2072) * Modify controller DTOs * Can check duplicates on server side * Remove deviceassetid and deviceid * Remove device ids from file uploader * Add db migration for removed device ids * Don't sanitize checksum * Convert asset checksum to string * Make checksum not optional for asset * Use enums when rejecting duplicates * Cleanup * Return of the device id, but optional * Don't use deviceId for upload folder * Use checksum in thumb path * Only use asset id in thumb path * Openapi generation * Put deviceAssetId back in asset response dto * Add missing checksum in test fixture * Add another missing checksum in test fixture * Cleanup asset repository * Add back previous /exists endpoint * Require checksum to not be null * Correctly set deviceId in db * Remove index * Fix compilation errors * Make device id nullabel in asset response dto * Reduce PR scope * Revert asset service * Reorder imports * Reorder imports * Reduce PR scope * Reduce PR scope * Reduce PR scope * Reduce PR scope * Reduce PR scope * Update openapi * Reduce PR scope * refactor: asset bulk upload check * chore: regenreate open-api * chore: fix tests * chore: tests * update migrations and regenerate api * Feat: use checksum in web file uploader * Change to wasm-crypto * Use crypto api for checksumming in web uploader * Minor cleanup of file upload * feat(web): pause and resume jobs * Make device asset id not nullable again * Cleanup * Device id not nullable in response dto * Update API specs * Bump api specs * Remove old TODO comment * Remove NOT NULL constraint on checksum index * Fix requested pubspec changes * Remove unneeded import * Update server/apps/immich/src/api-v1/asset/asset.service.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Update server/apps/immich/src/api-v1/asset/asset-repository.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Remove unneeded check * Update server/apps/immich/src/api-v1/asset/asset-repository.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Remove hashing in the web uploader * Cleanup file uploader * Remove varchar from asset entity fields * Return 200 from bulk upload check * Put device asset id back into asset repository * Merge migrations * Revert pubspec lock * Update openapi specs * Merge upstream changes * Fix failing asset service tests * Fix formatting issue * Cleanup migrations * Remove newline from pubspec * Revert newline * Checkout main version * Revert again * Only return AssetCheck --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2023-05-24 23:08:21 +02:00
getAssetsByChecksums: jest.fn(),
getExistingAssets: jest.fn(),
feat(server): support for read-only assets and importing existing items in the filesystem (#2715) * Added read-only flag for assets, endpoint to trigger file import vs upload * updated fixtures with new property * if upload is 'read-only', ensure there is no existing asset at the designated originalPath * added test for file import as well as detecting existing image at read-only destination location * Added storage service test for a case where it should not move read-only assets * upload doesn't need the read-only flag available, just importing * default isReadOnly on import endpoint to true * formatting fixes * create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation * updated code to reflect changes in MR * fixed read stream promise return type * new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates * refactor: import asset * chore: open api * chore: tests * Added externalPath support for individual users, updated UI to allow this to be set by admin * added missing var for externalPath in ui * chore: open api * fix: compilation issues * fix: server test * built api, fixed user-response dto to include externalPath * reverted accidental commit * bad commit of duplicate externalPath in user response dto * fixed tests to include externalPath on expected result * fix: unit tests * centralized supported filetypes, perform file type checking of asset and sidecar during file import process * centralized supported filetype check method to keep regex DRY * fixed typo * combined migrations into one * update api * Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not * update mimetype * Fixed detect correct mimetype * revert asset-upload config * reverted domain.constant * refactor * fix mime-type issue * fix format --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
getByOriginalPath: jest.fn(),
};
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
jobMock = newJobRepositoryMock();
libraryMock = newLibraryRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
2024-03-21 14:07:47 +01:00
sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoStillAsset.id)
.mockResolvedValue(assetStub.livePhotoStillAsset);
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoMotionAsset.id)
.mockResolvedValue(assetStub.livePhotoMotionAsset);
});
describe('uploadFile', () => {
it('should handle a file upload', async () => {
const assetEntity = _getAsset_1();
const file = {
uuid: 'random-uuid',
originalPath: 'fake_path/asset_1.jpeg',
mimeType: 'image/jpeg',
checksum: Buffer.from('file hash', 'utf8'),
originalName: 'asset_1.jpeg',
size: 42,
};
const dto = _getCreateAssetDto();
assetMock.create.mockResolvedValue(assetEntity);
chore(server): Check more permissions in bulk (#5315) Modify Access repository, to evaluate `authDevice`, `library`, `partner`, `person`, and `timeline` permissions in bulk. Queries have been validated to match what they currently generate for single ids. As an extra performance improvement, we now use a custom QueryBuilder for the Partners queries, to avoid the eager relationships that add unneeded `LEFT JOIN` clauses. We only filter based on the ids present in the `partners` table, so those joins can be avoided. Queries: * `library` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" = $1 AND "LibraryEntity"."ownerId" = $2 AND "LibraryEntity"."deletedAt" IS NULL ) LIMIT 1 -- After SELECT "LibraryEntity"."id" AS "LibraryEntity_id" FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" IN ($1, $2) AND "LibraryEntity"."ownerId" = $3 AND "LibraryEntity"."deletedAt" IS NULL ``` * `library` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `authDevice` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" = $2 ) LIMIT 1 -- After SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id" FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" IN ($2, $3) ``` * `timeline` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `person` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "person" "PersonEntity" WHERE "PersonEntity"."id" = $1 AND "PersonEntity"."ownerId" = $2 ) LIMIT 1 -- After SELECT "PersonEntity"."id" AS "PersonEntity_id" FROM "person" "PersonEntity" WHERE "PersonEntity"."id" IN ($1, $2) AND "PersonEntity"."ownerId" = $3 ``` * `partner` update access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ```
2023-11-26 13:50:41 +01:00
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
expect(assetMock.create).toHaveBeenCalled();
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
expect(storageMock.utimes).toHaveBeenCalledWith(
file.originalPath,
expect.any(Date),
new Date(dto.fileModifiedAt),
);
});
it('should handle a duplicate', async () => {
const file = {
uuid: 'random-uuid',
originalPath: 'fake_path/asset_1.jpeg',
mimeType: 'image/jpeg',
checksum: Buffer.from('file hash', 'utf8'),
originalName: 'asset_1.jpeg',
size: 0,
};
const dto = _getCreateAssetDto();
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetMock.create.mockRejectedValue(error);
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
chore(server): Check more permissions in bulk (#5315) Modify Access repository, to evaluate `authDevice`, `library`, `partner`, `person`, and `timeline` permissions in bulk. Queries have been validated to match what they currently generate for single ids. As an extra performance improvement, we now use a custom QueryBuilder for the Partners queries, to avoid the eager relationships that add unneeded `LEFT JOIN` clauses. We only filter based on the ids present in the `partners` table, so those joins can be avoided. Queries: * `library` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" = $1 AND "LibraryEntity"."ownerId" = $2 AND "LibraryEntity"."deletedAt" IS NULL ) LIMIT 1 -- After SELECT "LibraryEntity"."id" AS "LibraryEntity_id" FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" IN ($1, $2) AND "LibraryEntity"."ownerId" = $3 AND "LibraryEntity"."deletedAt" IS NULL ``` * `library` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `authDevice` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" = $2 ) LIMIT 1 -- After SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id" FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" IN ($2, $3) ``` * `timeline` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `person` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "person" "PersonEntity" WHERE "PersonEntity"."id" = $1 AND "PersonEntity"."ownerId" = $2 ) LIMIT 1 -- After SELECT "PersonEntity"."id" AS "PersonEntity_id" FROM "person" "PersonEntity" WHERE "PersonEntity"."id" IN ($1, $2) AND "PersonEntity"."ownerId" = $3 ``` * `partner` update access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ```
2023-11-26 13:50:41 +01:00
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES,
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
});
expect(userMock.updateUsage).not.toHaveBeenCalled();
});
it('should handle a live photo', async () => {
const dto = _getCreateAssetDto();
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
chore(server): Check more permissions in bulk (#5315) Modify Access repository, to evaluate `authDevice`, `library`, `partner`, `person`, and `timeline` permissions in bulk. Queries have been validated to match what they currently generate for single ids. As an extra performance improvement, we now use a custom QueryBuilder for the Partners queries, to avoid the eager relationships that add unneeded `LEFT JOIN` clauses. We only filter based on the ids present in the `partners` table, so those joins can be avoided. Queries: * `library` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" = $1 AND "LibraryEntity"."ownerId" = $2 AND "LibraryEntity"."deletedAt" IS NULL ) LIMIT 1 -- After SELECT "LibraryEntity"."id" AS "LibraryEntity_id" FROM "libraries" "LibraryEntity" WHERE "LibraryEntity"."id" IN ($1, $2) AND "LibraryEntity"."ownerId" = $3 AND "LibraryEntity"."deletedAt" IS NULL ``` * `library` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `authDevice` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" = $2 ) LIMIT 1 -- After SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id" FROM "user_token" "UserTokenEntity" WHERE "UserTokenEntity"."userId" = $1 AND "UserTokenEntity"."id" IN ($2, $3) ``` * `timeline` partner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ``` * `person` owner access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "person" "PersonEntity" WHERE "PersonEntity"."id" = $1 AND "PersonEntity"."ownerId" = $2 ) LIMIT 1 -- After SELECT "PersonEntity"."id" AS "PersonEntity_id" FROM "person" "PersonEntity" WHERE "PersonEntity"."id" IN ($1, $2) AND "PersonEntity"."ownerId" = $3 ``` * `partner` update access: ```sql -- Before SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS ( SELECT 1 FROM "partners" "PartnerEntity" LEFT JOIN "users" "PartnerEntity__sharedBy" ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById" AND "PartnerEntity__sharedBy"."deletedAt" IS NULL LEFT JOIN "users" "PartnerEntity__sharedWith" ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId" AND "PartnerEntity__sharedWith"."deletedAt" IS NULL WHERE "PartnerEntity"."sharedWithId" = $1 AND "PartnerEntity"."sharedById" = $2 ) LIMIT 1 -- After SELECT "partner"."sharedById" AS "partner_sharedById", "partner"."sharedWithId" AS "partner_sharedWithId" FROM "partners" "partner" WHERE "partner"."sharedById" IN ($1, $2) AND "partner"."sharedWithId" = $3 ```
2023-11-26 13:50:41 +01:00
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
await expect(
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
).resolves.toEqual({
duplicate: false,
id: 'live-photo-still-asset',
});
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.METADATA_EXTRACTION,
data: { id: assetStub.livePhotoMotionAsset.id, source: 'upload' },
},
],
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
]);
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111);
expect(storageMock.utimes).toHaveBeenCalledWith(
fileStub.livePhotoStill.originalPath,
expect.any(Date),
new Date(dto.fileModifiedAt),
);
expect(storageMock.utimes).toHaveBeenCalledWith(
fileStub.livePhotoMotion.originalPath,
expect.any(Date),
new Date(dto.fileModifiedAt),
);
});
});
describe('bulkUploadCheck', () => {
it('should accept hex and base64 checksums', async () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
{ id: 'asset-1', checksum: file1 },
{ id: 'asset-2', checksum: file2 },
]);
await expect(
sut.bulkUploadCheck(authStub.admin, {
assets: [
{ id: '1', checksum: file1.toString('hex') },
{ id: '2', checksum: file2.toString('base64') },
],
}),
).resolves.toEqual({
results: [
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
],
});
expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
});
});
});