Merge remote-tracking branch 'origin/main' into feat/yucca-integration

This commit is contained in:
izzy
2026-04-16 12:04:38 +01:00
523 changed files with 2711 additions and 54886 deletions
+7 -7
View File
@@ -563,9 +563,9 @@ describe(AlbumService.name, () => {
},
]);
await sut.get(AuthFactory.create(album.owner), album.id, {});
await sut.get(AuthFactory.create(album.owner), album.id);
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true });
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id]));
});
@@ -584,9 +584,9 @@ describe(AlbumService.name, () => {
]);
const auth = AuthFactory.from().sharedLink().build();
await sut.get(auth, album.id, {});
await sut.get(auth, album.id);
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true });
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id]));
});
@@ -605,9 +605,9 @@ describe(AlbumService.name, () => {
},
]);
await sut.get(AuthFactory.create(user), album.id, {});
await sut.get(AuthFactory.create(user), album.id);
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true });
expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false });
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
user.id,
new Set([album.id]),
@@ -617,7 +617,7 @@ describe(AlbumService.name, () => {
it('should throw an error for no access', async () => {
const auth = AuthFactory.create();
await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.get(auth, 'album-123')).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123']));
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
+18 -11
View File
@@ -1,7 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import {
AddUsersDto,
AlbumInfoDto,
AlbumResponseDto,
AlbumsAddAssetsDto,
AlbumsAddAssetsResponseDto,
@@ -10,13 +9,12 @@ import {
GetAlbumsDto,
mapAlbum,
MapAlbumDto,
mapAlbumWithAssets,
mapAlbumWithoutAssets,
UpdateAlbumDto,
UpdateAlbumUserDto,
} from 'src/dtos/album.dto';
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 { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
import { BaseService } from 'src/services/base.service';
@@ -63,7 +61,7 @@ export class AlbumService extends BaseService {
}
return albums.map((album) => ({
...mapAlbumWithoutAssets(album),
...mapAlbum(album),
sharedLinks: undefined,
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
@@ -73,11 +71,10 @@ export class AlbumService extends BaseService {
}));
}
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
async get(auth: AuthDto, id: string): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets });
const album = await this.findOrFail(id, { withAssets: false });
const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]);
const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0;
@@ -85,7 +82,7 @@ export class AlbumService extends BaseService {
const isShared = hasSharedUsers || hasSharedLink;
return {
...mapAlbum(album, withAssets, auth),
...mapAlbum(album),
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
assetCount: albumMetadataForIds?.assetCount ?? 0,
@@ -94,6 +91,16 @@ export class AlbumService extends BaseService {
};
}
async getMapMarkers(auth: AuthDto, id: string): Promise<MapMarkerResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
if (auth.sharedLink && !auth.sharedLink.showExif) {
return [];
}
return this.mapRepository.getAlbumMapMarkers(id);
}
async create(auth: AuthDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
const albumUsers = dto.albumUsers || [];
@@ -133,7 +140,7 @@ export class AlbumService extends BaseService {
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
}
return mapAlbumWithAssets(album);
return mapAlbum(album);
}
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
@@ -156,7 +163,7 @@ export class AlbumService extends BaseService {
order: dto.order,
});
return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets });
return mapAlbum({ ...updatedAlbum, assets: album.assets });
}
async delete(auth: AuthDto, id: string): Promise<void> {
@@ -294,7 +301,7 @@ export class AlbumService extends BaseService {
await this.eventRepository.emit('AlbumInvite', { id, userId });
}
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
return this.findOrFail(id, { withAssets: true }).then(mapAlbum);
}
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
+7 -7
View File
@@ -1,6 +1,6 @@
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { ApiKey } from 'src/database';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum';
import { BaseService } from 'src/services/base.service';
@@ -8,7 +8,7 @@ import { isGranted } from 'src/utils/access';
@Injectable()
export class ApiKeyService extends BaseService {
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
async create(auth: AuthDto, dto: ApiKeyCreateDto): Promise<ApiKeyCreateResponseDto> {
const token = this.cryptoRepository.randomBytesAsText(32);
const hashed = this.cryptoRepository.hashSha256(token);
@@ -26,7 +26,7 @@ export class ApiKeyService extends BaseService {
return { secret: token, apiKey: this.map(entity) };
}
async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> {
async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise<ApiKeyResponseDto> {
const exists = await this.apiKeyRepository.getById(auth.user.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
@@ -54,7 +54,7 @@ export class ApiKeyService extends BaseService {
await this.apiKeyRepository.delete(auth.user.id, id);
}
async getMine(auth: AuthDto): Promise<APIKeyResponseDto> {
async getMine(auth: AuthDto): Promise<ApiKeyResponseDto> {
if (!auth.apiKey) {
throw new ForbiddenException('Not authenticated with an API Key');
}
@@ -67,7 +67,7 @@ export class ApiKeyService extends BaseService {
return this.map(key);
}
async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
async getById(auth: AuthDto, id: string): Promise<ApiKeyResponseDto> {
const key = await this.apiKeyRepository.getById(auth.user.id, id);
if (!key) {
throw new BadRequestException('API Key not found');
@@ -75,12 +75,12 @@ export class ApiKeyService extends BaseService {
return this.map(key);
}
async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> {
async getAll(auth: AuthDto): Promise<ApiKeyResponseDto[]> {
const keys = await this.apiKeyRepository.getByUserId(auth.user.id);
return keys.map((key) => this.map(key));
}
private map(entity: ApiKey): APIKeyResponseDto {
private map(entity: ApiKey): ApiKeyResponseDto {
return {
id: entity.id,
name: entity.name,
@@ -145,8 +145,6 @@ const uploadTests = [
];
const createDto = Object.freeze({
deviceAssetId: 'deviceAssetId',
deviceId: 'deviceId',
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false,
@@ -156,8 +154,6 @@ const createDto = Object.freeze({
const assetEntity = Object.freeze({
id: 'id_1',
ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1',
deviceId: 'device_id_1',
type: AssetType.Video,
originalPath: 'fake_path/asset_1.jpeg',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
@@ -765,17 +761,6 @@ describe(AssetMediaService.name, () => {
});
});
describe('checkExistingAssets', () => {
it('should get existing asset ids', async () => {
mocks.asset.getByDeviceIds.mockResolvedValue(['42']);
await expect(
sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
).resolves.toEqual({ existingIds: ['42'] });
expect(mocks.asset.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
});
});
describe('bulkUploadCheck', () => {
it('should accept hex and base64 checksums', async () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
@@ -9,14 +9,12 @@ import {
AssetMediaStatus,
AssetRejectReason,
AssetUploadAction,
CheckExistingAssetsResponseDto,
} from 'src/dtos/asset-media-response.dto';
import {
AssetBulkUploadCheckDto,
AssetMediaCreateDto,
AssetMediaOptionsDto,
AssetMediaSize,
CheckExistingAssetsDto,
UploadFieldName,
} from 'src/dtos/asset-media.dto';
import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto';
@@ -251,18 +249,6 @@ export class AssetMediaService extends BaseService {
});
}
async checkExistingAssets(
auth: AuthDto,
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const existingIds = await this.assetRepository.getByDeviceIds(
auth.user.id,
checkExistingAssetsDto.deviceId,
checkExistingAssetsDto.deviceAssetIds,
);
return { existingIds };
}
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
@@ -340,9 +326,6 @@ export class AssetMediaService extends BaseService {
checksumAlgorithm: ChecksumAlgorithm.sha1File,
originalPath: file.originalPath,
deviceAssetId: dto.deviceAssetId,
deviceId: dto.deviceId,
fileCreatedAt: dto.fileCreatedAt,
fileModifiedAt: dto.fileModifiedAt,
localDateTime: dto.fileCreatedAt,
-14
View File
@@ -685,20 +685,6 @@ describe(AssetService.name, () => {
});
});
describe('getUserAssetsByDeviceId', () => {
it('get assets by device id', async () => {
const assets = [AssetFactory.create(), AssetFactory.create()];
mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
const deviceId = 'device-id';
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
expect(result.length).toEqual(2);
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
});
});
describe('upsertMetadata', () => {
it('should throw a bad request exception if duplicate keys are sent', async () => {
const asset = AssetFactory.create();
-4
View File
@@ -59,10 +59,6 @@ export class AssetService extends BaseService {
return mapStats(stats);
}
async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
-26
View File
@@ -1,26 +0,0 @@
import { JobStatus } from 'src/enum';
import { AuditService } from 'src/services/audit.service';
import { newTestService, ServiceMocks } from 'test/utils';
describe(AuditService.name, () => {
let sut: AuditService;
let mocks: ServiceMocks;
beforeEach(() => {
({ sut, mocks } = newTestService(AuditService));
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('handleCleanup', () => {
it('should delete old audit entries', async () => {
mocks.audit.removeBefore.mockResolvedValue();
await expect(sut.handleCleanup()).resolves.toBe(JobStatus.Success);
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
});
});
});
-15
View File
@@ -1,15 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { OnJob } from 'src/decorators';
import { JobName, JobStatus, QueueName } from 'src/enum';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class AuditService extends BaseService {
@OnJob({ name: JobName.AuditLogCleanup, queue: QueueName.BackgroundTask })
async handleCleanup(): Promise<JobStatus> {
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
return JobStatus.Success;
}
}
-3
View File
@@ -14,7 +14,6 @@ import { AppRepository } from 'src/repositories/app.repository';
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { CronRepository } from 'src/repositories/cron.repository';
import { CryptoRepository } from 'src/repositories/crypto.repository';
@@ -72,7 +71,6 @@ export const BASE_SERVICE_DEPENDENCIES = [
AssetRepository,
AssetEditRepository,
AssetJobRepository,
AuditRepository,
ConfigRepository,
CronRepository,
CryptoRepository,
@@ -131,7 +129,6 @@ export class BaseService {
protected assetRepository: AssetRepository,
protected assetEditRepository: AssetEditRepository,
protected assetJobRepository: AssetJobRepository,
protected auditRepository: AuditRepository,
protected configRepository: ConfigRepository,
protected cronRepository: CronRepository,
protected cryptoRepository: CryptoRepository,
-2
View File
@@ -4,7 +4,6 @@ import { ApiKeyService } from 'src/services/api-key.service';
import { ApiService } from 'src/services/api.service';
import { AssetMediaService } from 'src/services/asset-media.service';
import { AssetService } from 'src/services/asset.service';
import { AuditService } from 'src/services/audit.service';
import { AuthAdminService } from 'src/services/auth-admin.service';
import { AuthService } from 'src/services/auth.service';
import { CliService } from 'src/services/cli.service';
@@ -55,7 +54,6 @@ export const services = [
ApiService,
AssetMediaService,
AssetService,
AuditService,
AuthService,
AuthAdminService,
CliService,
@@ -570,7 +570,6 @@ describe(LibraryService.name, () => {
ownerId: library.ownerId,
libraryId: library.id,
originalPath: '/data/user1/photo.jpg',
deviceId: 'Library Import',
type: AssetType.Image,
originalFileName: 'photo.jpg',
isExternal: true,
+1 -4
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { Insertable } from 'kysely';
import { R_OK } from 'node:constants';
import { Stats } from 'node:fs';
import path, { basename, isAbsolute, parse } from 'node:path';
import path, { isAbsolute, parse } from 'node:path';
import picomatch from 'picomatch';
import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
@@ -416,9 +416,6 @@ export class LibraryService extends BaseService {
fileCreatedAt: stat.mtime,
fileModifiedAt: stat.mtime,
localDateTime: stat.mtime,
// TODO: device asset id is deprecated, remove it
deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''),
deviceId: 'Library Import',
type: mimeTypes.isVideo(assetPath) ? AssetType.Video : AssetType.Image,
originalFileName: parse(assetPath).base,
isExternal: true,
@@ -654,8 +654,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
@@ -708,8 +706,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
@@ -762,8 +758,6 @@ describe(MetadataService.name, () => {
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
id: motionAsset.id,
-2
View File
@@ -681,8 +681,6 @@ export class MetadataService extends BaseService {
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
visibility: AssetVisibility.Hidden,
deviceAssetId: 'NONE',
deviceId: 'NONE',
});
isNewMotionAsset = true;
@@ -42,7 +42,6 @@ describe(QueueService.name, () => {
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
{ name: JobName.MemoryGenerate },
{ name: JobName.UserSyncUsage },
{ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } },
-1
View File
@@ -270,7 +270,6 @@ export class QueueService extends BaseService {
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
);
}
-98
View File
@@ -1,98 +0,0 @@
import { mapAsset } from 'src/dtos/asset-response.dto';
import { SyncService } from 'src/services/sync.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { PartnerFactory } from 'test/factories/partner.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
const untilDate = new Date(2024);
const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: true };
describe(SyncService.name, () => {
let sut: SyncService;
let mocks: ServiceMocks;
beforeEach(() => {
({ sut, mocks } = newTestService(SyncService));
});
it('should exist', () => {
expect(sut).toBeDefined();
});
describe('getAllAssetsForUserFullSync', () => {
it('should return a list of all assets owned by the user', async () => {
const [asset1, asset2] = [
AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(),
AssetFactory.from().owner(authStub.user1.user).build(),
];
mocks.asset.getAllForUserFullSync.mockResolvedValue([getForAsset(asset1), getForAsset(asset2)]);
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
mapAsset(getForAsset(asset1), mapAssetOpts),
mapAsset(getForAsset(asset2), mapAssetOpts),
]);
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
ownerId: authStub.user1.user.id,
updatedUntil: untilDate,
limit: 2,
});
});
});
describe('getChangesForDeltaSync', () => {
it('should return a response requiring a full sync when partners are out of sync', async () => {
const partner = PartnerFactory.create();
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
});
it('should return a response requiring a full sync when last sync was too long ago', async () => {
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
});
it('should return a response requiring a full sync when there are too many changes', async () => {
const asset = AssetFactory.create();
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getChangedDeltaSync.mockResolvedValue(
Array.from<ReturnType<typeof getForAsset>>({ length: 10_000 }).fill(getForAsset(asset)),
);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(0);
});
it('should return a response with changes and deletions', async () => {
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]);
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({
needsFullSync: false,
upserted: [mapAsset(getForAsset(asset), mapAssetOpts)],
deleted: [deletedAsset.id],
});
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1);
});
});
});
+1 -82
View File
@@ -2,14 +2,9 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
import { Insertable } from 'kysely';
import { DateTime, Duration } from 'luxon';
import { Writable } from 'node:stream';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { OnJob } from 'src/decorators';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
AssetDeltaSyncDto,
AssetDeltaSyncResponseDto,
AssetFullSyncDto,
SyncAckDeleteDto,
SyncAckSetDto,
syncAssetFaceV2ToV1,
@@ -17,23 +12,12 @@ import {
SyncItem,
SyncStreamDto,
} from 'src/dtos/sync.dto';
import {
AssetVisibility,
DatabaseAction,
EntityType,
JobName,
Permission,
QueueName,
SyncEntityType,
SyncRequestType,
} from 'src/enum';
import { JobName, QueueName, SyncEntityType, SyncRequestType } from 'src/enum';
import { SyncQueryOptions } from 'src/repositories/sync.repository';
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
import { BaseService } from 'src/services/base.service';
import { SyncAck } from 'src/types';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { setIsEqual } from 'src/utils/set';
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
@@ -66,7 +50,6 @@ const sendEntityBackfillCompleteAck = (response: Writable, ackType: SyncEntityTy
send(response, { type: SyncEntityType.SyncAckV1, data: {}, ackType, ids: [id, COMPLETE_ID] });
};
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
export const SYNC_TYPES_ORDER = [
SyncRequestType.AuthUsersV1,
SyncRequestType.UsersV1,
@@ -887,68 +870,4 @@ export class SyncService extends BaseService {
},
]);
}
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
// mobile implementation is faster if this is a single id
const userId = dto.userId || auth.user.id;
await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [userId] });
const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId,
updatedUntil: dto.updatedUntil,
lastId: dto.lastId,
limit: dto.limit,
});
return assets.map((a) => mapAsset(a, { auth, stripMetadata: false, withStack: true }));
}
async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
// app has not synced in the last 100 days
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter));
if (duration > AUDIT_LOG_MAX_DURATION) {
return FULL_SYNC;
}
// app does not have the correct partners synced
const partnerIds = await getMyPartnerIds({ userId: auth.user.id, repository: this.partnerRepository });
const userIds = [auth.user.id, ...partnerIds];
if (!setIsEqual(new Set(userIds), new Set(dto.userIds))) {
return FULL_SYNC;
}
await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: dto.userIds });
const limit = 10_000;
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
// too many changes, need to do a full sync
if (upserted.length === limit) {
return FULL_SYNC;
}
const deleted = await this.auditRepository.getAfter(dto.updatedAfter, {
userIds,
entityType: EntityType.Asset,
action: DatabaseAction.Delete,
});
const result = {
needsFullSync: false,
upserted: upserted
// do not return archived assets for partner users
.filter(
(a) =>
a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.Timeline),
)
.map((a) =>
mapAsset(a, {
auth,
stripMetadata: false,
// ignore stacks for non partner users
withStack: a.ownerId === auth.user.id,
}),
),
deleted,
};
return result;
}
}