Merge remote-tracking branch 'origin/main' into chore/pnpm_alt
This commit is contained in:
+1
-1
@@ -97,7 +97,7 @@ WORKDIR /workspaces/immich
|
||||
RUN rm -rf /tmp/build
|
||||
|
||||
FROM dev-container-server AS dev-container-mobile
|
||||
|
||||
USER root
|
||||
# Enable multiarch for arm64 if necessary
|
||||
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
||||
sudo dpkg --add-architecture amd64 && \
|
||||
|
||||
Generated
+18933
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -136,7 +136,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { repositories } from 'src/repositories';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { getKyselyConfig } from 'src/utils/database';
|
||||
|
||||
@@ -111,7 +112,7 @@ class SqlGenerator {
|
||||
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
|
||||
|
||||
// nested repositories
|
||||
if (Repository.name === AccessRepository.name) {
|
||||
if (Repository.name === AccessRepository.name || Repository.name === SyncRepository.name) {
|
||||
for (const key of Object.keys(instance)) {
|
||||
const subInstance = (instance as any)[key];
|
||||
data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`)));
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import request from 'supertest';
|
||||
import { mediumFactory } from 'test/medium.factory';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
@@ -132,6 +133,50 @@ describe(AuthController.name, () => {
|
||||
expect(status).toEqual(201);
|
||||
expect(service.login).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@local' }), expect.anything());
|
||||
});
|
||||
|
||||
it('should auth cookies on a secure connection', async () => {
|
||||
const loginResponse = mediumFactory.loginResponse();
|
||||
service.login.mockResolvedValue(loginResponse);
|
||||
const { status, body, headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ name: 'admin', email: 'admin@local', password: 'password' });
|
||||
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual(loginResponse);
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
|
||||
`immich_access_token=${loginResponse.accessToken}`,
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_auth_type=password',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_is_authenticated=true',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/auth/logout');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/change-password', () => {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { MemoryController } from 'src/controllers/memory.controller';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import request from 'supertest';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(MemoryController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(MemoryService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(MemoryController, [{ provide: MemoryService, useValue: service }]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.resetAllMocks();
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
describe('GET /memories', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get('/memories');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /memories', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/memories');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate data when type is on this day', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/memories')
|
||||
.send({
|
||||
type: 'on_this_day',
|
||||
data: {},
|
||||
memoryAt: new Date(2021).toISOString(),
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /memories/statistics', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get('/memories/statistics');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /memories/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/memories/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /memories/:id/assets', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}/assets`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/memories/${factory.uuid()}/assets`)
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id/assets', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}/assets`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require a valid asset id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.delete(`/memories/${factory.uuid()}/assets`)
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,6 +60,29 @@ describe(PersonController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /people', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete('/people');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require uuids in the body', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.delete('/people')
|
||||
.send({ ids: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.delete(`/people`)
|
||||
.send({ ids: [factory.uuid()] });
|
||||
expect(status).toBe(204);
|
||||
expect(service.deleteAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /people/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`);
|
||||
@@ -156,6 +179,25 @@ describe(PersonController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /people/:id', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
|
||||
it('should respond with 204', async () => {
|
||||
const { status } = await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`);
|
||||
expect(status).toBe(204);
|
||||
expect(service.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /people/:id/merge', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`);
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Next,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
AssetFaceUpdateDto,
|
||||
@@ -49,6 +62,13 @@ export class PersonController {
|
||||
return this.service.updateAll(auth, dto);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PERSON_DELETE })
|
||||
deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
|
||||
return this.service.deleteAll(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.PERSON_READ })
|
||||
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
||||
@@ -65,6 +85,13 @@ export class PersonController {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Authenticated({ permission: Permission.PERSON_DELETE })
|
||||
deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { SyncController } from 'src/controllers/sync.controller';
|
||||
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import request from 'supertest';
|
||||
import { errorDto } from 'test/medium/responses';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(SyncController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const syncService = mockBaseService(SyncService);
|
||||
const errorService = { handleError: vi.fn() };
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(SyncController, [
|
||||
{ provide: SyncService, useValue: syncService },
|
||||
{ provide: GlobalExceptionFilter, useValue: errorService },
|
||||
]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
syncService.resetAllMocks();
|
||||
errorService.handleError.mockReset();
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
describe('POST /sync/stream', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/sync/stream');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require sync request type enums', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/sync/stream')
|
||||
.send({ types: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /sync/ack', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get('/sync/ack');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sync/ack', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/sync/ack');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not allow more than 1,000 entries', async () => {
|
||||
const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`);
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['acks must contain no more than 1000 elements']));
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /sync/ack', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).delete('/sync/ack');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should require sync response type enums', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.delete('/sync/ack')
|
||||
.send({ types: ['invalid'] });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]),
|
||||
);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+16
-23
@@ -13,7 +13,7 @@ import {
|
||||
UserAvatarColor,
|
||||
UserStatus,
|
||||
} from 'src/enum';
|
||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
@@ -95,7 +95,7 @@ export type Memory = {
|
||||
showAt: Date | null;
|
||||
hideAt: Date | null;
|
||||
type: MemoryType;
|
||||
data: OnThisDayData;
|
||||
data: object;
|
||||
ownerId: string;
|
||||
isSaved: boolean;
|
||||
assets: MapAsset[];
|
||||
@@ -340,27 +340,21 @@ export const columns = {
|
||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
||||
syncAsset: [
|
||||
'id',
|
||||
'ownerId',
|
||||
'originalFileName',
|
||||
'thumbhash',
|
||||
'checksum',
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'localDateTime',
|
||||
'type',
|
||||
'deletedAt',
|
||||
'isFavorite',
|
||||
'visibility',
|
||||
'updateId',
|
||||
'duration',
|
||||
],
|
||||
syncAlbumUser: [
|
||||
'albums_shared_users_users.albumsId as albumId',
|
||||
'albums_shared_users_users.usersId as userId',
|
||||
'albums_shared_users_users.role',
|
||||
'albums_shared_users_users.updateId',
|
||||
'assets.id',
|
||||
'assets.ownerId',
|
||||
'assets.originalFileName',
|
||||
'assets.thumbhash',
|
||||
'assets.checksum',
|
||||
'assets.fileCreatedAt',
|
||||
'assets.fileModifiedAt',
|
||||
'assets.localDateTime',
|
||||
'assets.type',
|
||||
'assets.deletedAt',
|
||||
'assets.isFavorite',
|
||||
'assets.visibility',
|
||||
'assets.duration',
|
||||
],
|
||||
syncAlbumUser: ['album_users.albumsId as albumId', 'album_users.usersId as userId', 'album_users.role'],
|
||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||
syncAssetExif: [
|
||||
'exif.assetId',
|
||||
@@ -388,7 +382,6 @@ export const columns = {
|
||||
'exif.profileDescription',
|
||||
'exif.rating',
|
||||
'exif.fps',
|
||||
'exif.updateId',
|
||||
],
|
||||
exif: [
|
||||
'exif.assetId',
|
||||
|
||||
Vendored
+18
-8
@@ -19,8 +19,11 @@ import {
|
||||
SourceType,
|
||||
SyncEntityType,
|
||||
} from 'src/enum';
|
||||
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
|
||||
import { MemoryAuditTable } from 'src/schema/tables/memory-audit.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
|
||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
|
||||
|
||||
@@ -92,6 +95,15 @@ export interface AlbumsAssetsAssets {
|
||||
albumsId: string;
|
||||
assetsId: string;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
updateId: Generated<string>;
|
||||
}
|
||||
|
||||
export interface AlbumAssetsAudit {
|
||||
deletedAt: Generated<Timestamp>;
|
||||
id: Generated<string>;
|
||||
albumId: string;
|
||||
assetId: string;
|
||||
}
|
||||
|
||||
export interface AlbumsSharedUsersUsers {
|
||||
@@ -269,7 +281,7 @@ export interface Libraries {
|
||||
|
||||
export interface Memories {
|
||||
createdAt: Generated<Timestamp>;
|
||||
data: OnThisDayData;
|
||||
data: object;
|
||||
deletedAt: Timestamp | null;
|
||||
hideAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
@@ -298,11 +310,6 @@ export interface Notifications {
|
||||
readAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface MemoriesAssetsAssets {
|
||||
assetsId: string;
|
||||
memoriesId: string;
|
||||
}
|
||||
|
||||
export interface Migrations {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
@@ -487,6 +494,7 @@ export interface DB {
|
||||
albums: Albums;
|
||||
albums_audit: AlbumsAudit;
|
||||
albums_assets_assets: AlbumsAssetsAssets;
|
||||
album_assets_audit: AlbumAssetsAudit;
|
||||
albums_shared_users_users: AlbumsSharedUsersUsers;
|
||||
album_users_audit: AlbumUsersAudit;
|
||||
api_keys: ApiKeys;
|
||||
@@ -502,7 +510,9 @@ export interface DB {
|
||||
geodata_places: GeodataPlaces;
|
||||
libraries: Libraries;
|
||||
memories: Memories;
|
||||
memories_assets_assets: MemoriesAssetsAssets;
|
||||
memories_audit: MemoryAuditTable;
|
||||
memories_assets_assets: MemoryAssetTable;
|
||||
memory_assets_audit: MemoryAssetAuditTable;
|
||||
migrations: Migrations;
|
||||
notifications: Notifications;
|
||||
move_history: MoveHistory;
|
||||
|
||||
+92
-19
@@ -1,7 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { ArrayMaxSize, IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AlbumUserRole, AssetOrder, AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetOrder,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
SyncEntityType,
|
||||
SyncRequestType,
|
||||
} from 'src/enum';
|
||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AssetFullSyncDto {
|
||||
@@ -34,6 +43,15 @@ export class AssetDeltaSyncResponseDto {
|
||||
deleted!: string[];
|
||||
}
|
||||
|
||||
export const extraSyncModels: Function[] = [];
|
||||
|
||||
export const ExtraModel = (): ClassDecorator => {
|
||||
return (object: Function) => {
|
||||
extraSyncModels.push(object);
|
||||
};
|
||||
};
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncUserV1 {
|
||||
id!: string;
|
||||
name!: string;
|
||||
@@ -41,21 +59,25 @@ export class SyncUserV1 {
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncUserDeleteV1 {
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncPartnerV1 {
|
||||
sharedById!: string;
|
||||
sharedWithId!: string;
|
||||
inTimeline!: boolean;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncPartnerDeleteV1 {
|
||||
sharedById!: string;
|
||||
sharedWithId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetV1 {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
@@ -74,10 +96,12 @@ export class SyncAssetV1 {
|
||||
visibility!: AssetVisibility;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetDeleteV1 {
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAssetExifV1 {
|
||||
assetId!: string;
|
||||
description!: string | null;
|
||||
@@ -116,15 +140,18 @@ export class SyncAssetExifV1 {
|
||||
fps!: number | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumDeleteV1 {
|
||||
albumId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumUserDeleteV1 {
|
||||
albumId!: string;
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumUserV1 {
|
||||
albumId!: string;
|
||||
userId!: string;
|
||||
@@ -132,6 +159,7 @@ export class SyncAlbumUserV1 {
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumV1 {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
@@ -145,6 +173,55 @@ export class SyncAlbumV1 {
|
||||
order!: AssetOrder;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumToAssetV1 {
|
||||
albumId!: string;
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAlbumToAssetDeleteV1 {
|
||||
albumId!: string;
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncMemoryV1 {
|
||||
id!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
deletedAt!: Date | null;
|
||||
ownerId!: string;
|
||||
@ApiProperty({ enumName: 'MemoryType', enum: MemoryType })
|
||||
type!: MemoryType;
|
||||
data!: object;
|
||||
isSaved!: boolean;
|
||||
memoryAt!: Date;
|
||||
seenAt!: Date | null;
|
||||
showAt!: Date | null;
|
||||
hideAt!: Date | null;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncMemoryDeleteV1 {
|
||||
memoryId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncMemoryAssetV1 {
|
||||
memoryId!: string;
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncMemoryAssetDeleteV1 {
|
||||
memoryId!: string;
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncAckV1 {}
|
||||
|
||||
export type SyncItem = {
|
||||
[SyncEntityType.UserV1]: SyncUserV1;
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
@@ -163,25 +240,20 @@ export type SyncItem = {
|
||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||
[SyncEntityType.SyncAckV1]: object;
|
||||
[SyncEntityType.AlbumAssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1;
|
||||
[SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1;
|
||||
[SyncEntityType.AlbumToAssetDeleteV1]: SyncAlbumToAssetDeleteV1;
|
||||
[SyncEntityType.MemoryV1]: SyncMemoryV1;
|
||||
[SyncEntityType.MemoryDeleteV1]: SyncMemoryDeleteV1;
|
||||
[SyncEntityType.MemoryToAssetV1]: SyncMemoryAssetV1;
|
||||
[SyncEntityType.MemoryToAssetDeleteV1]: SyncMemoryAssetDeleteV1;
|
||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||
};
|
||||
|
||||
const responseDtos = [
|
||||
SyncUserV1,
|
||||
SyncUserDeleteV1,
|
||||
SyncPartnerV1,
|
||||
SyncPartnerDeleteV1,
|
||||
SyncAssetV1,
|
||||
SyncAssetDeleteV1,
|
||||
SyncAssetExifV1,
|
||||
SyncAlbumV1,
|
||||
SyncAlbumDeleteV1,
|
||||
SyncAlbumUserV1,
|
||||
SyncAlbumUserDeleteV1,
|
||||
];
|
||||
|
||||
export const extraSyncModels = responseDtos;
|
||||
|
||||
export class SyncStreamDto {
|
||||
@IsEnum(SyncRequestType, { each: true })
|
||||
@ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true })
|
||||
@@ -195,6 +267,7 @@ export class SyncAckDto {
|
||||
}
|
||||
|
||||
export class SyncAckSetDto {
|
||||
@ArrayMaxSize(1000)
|
||||
@IsString({ each: true })
|
||||
acks!: string[];
|
||||
}
|
||||
|
||||
+24
-3
@@ -581,19 +581,24 @@ export enum SyncRequestType {
|
||||
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||
AlbumsV1 = 'AlbumsV1',
|
||||
AlbumUsersV1 = 'AlbumUsersV1',
|
||||
AlbumToAssetsV1 = 'AlbumToAssetsV1',
|
||||
AlbumAssetsV1 = 'AlbumAssetsV1',
|
||||
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
}
|
||||
|
||||
export enum SyncEntityType {
|
||||
UserV1 = 'UserV1',
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetDeleteV1 = 'AssetDeleteV1',
|
||||
AssetExifV1 = 'AssetExifV1',
|
||||
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
PartnerAssetV1 = 'PartnerAssetV1',
|
||||
PartnerAssetBackfillV1 = 'PartnerAssetBackfillV1',
|
||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||
@@ -602,10 +607,26 @@ export enum SyncEntityType {
|
||||
|
||||
AlbumV1 = 'AlbumV1',
|
||||
AlbumDeleteV1 = 'AlbumDeleteV1',
|
||||
|
||||
AlbumUserV1 = 'AlbumUserV1',
|
||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||
|
||||
AlbumAssetV1 = 'AlbumAssetV1',
|
||||
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||
AlbumAssetExifV1 = 'AlbumAssetExifV1',
|
||||
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||
|
||||
AlbumToAssetV1 = 'AlbumToAssetV1',
|
||||
AlbumToAssetDeleteV1 = 'AlbumToAssetDeleteV1',
|
||||
AlbumToAssetBackfillV1 = 'AlbumToAssetBackfillV1',
|
||||
|
||||
MemoryV1 = 'MemoryV1',
|
||||
MemoryDeleteV1 = 'MemoryDeleteV1',
|
||||
|
||||
MemoryToAssetV1 = 'MemoryToAssetV1',
|
||||
MemoryToAssetDeleteV1 = 'MemoryToAssetDeleteV1',
|
||||
|
||||
SyncAckV1 = 'SyncAckV1',
|
||||
}
|
||||
|
||||
|
||||
@@ -394,7 +394,6 @@ where
|
||||
"asset_job_status"."metadataExtractedAt" is null
|
||||
or "asset_job_status"."assetId" is null
|
||||
)
|
||||
and "assets"."visibility" != $1
|
||||
and "assets"."deletedAt" is null
|
||||
|
||||
-- AssetJobRepository.getForStorageTemplateJob
|
||||
|
||||
@@ -328,3 +328,12 @@ set
|
||||
"deletedAt" = $1
|
||||
where
|
||||
"asset_faces"."id" = $2
|
||||
|
||||
-- PersonRepository.getForPeopleDelete
|
||||
select
|
||||
"id",
|
||||
"thumbnailPath"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"id" in ($1)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- SyncCheckpointRepository.getAll
|
||||
select
|
||||
"type",
|
||||
"ack"
|
||||
from
|
||||
"session_sync_checkpoints"
|
||||
where
|
||||
"sessionId" = $1
|
||||
|
||||
-- SyncCheckpointRepository.deleteAll
|
||||
delete from "session_sync_checkpoints"
|
||||
where
|
||||
"sessionId" = $1
|
||||
@@ -1,172 +1,330 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- SyncRepository.getCheckpoints
|
||||
-- SyncRepository.album.getCreatedAfter
|
||||
select
|
||||
"type",
|
||||
"ack"
|
||||
"albumsId" as "id",
|
||||
"createId"
|
||||
from
|
||||
"session_sync_checkpoints"
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"sessionId" = $1
|
||||
|
||||
-- SyncRepository.deleteCheckpoints
|
||||
delete from "session_sync_checkpoints"
|
||||
where
|
||||
"sessionId" = $1
|
||||
|
||||
-- SyncRepository.getUserUpserts
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"deletedAt",
|
||||
"updateId"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"updatedAt" < now() - interval '1 millisecond'
|
||||
"usersId" = $1
|
||||
and "createId" >= $2
|
||||
and "createdAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
"createId" asc
|
||||
|
||||
-- SyncRepository.getUserDeletes
|
||||
-- SyncRepository.album.getDeletes
|
||||
select
|
||||
"id",
|
||||
"userId"
|
||||
"albumId"
|
||||
from
|
||||
"users_audit"
|
||||
"albums_audit"
|
||||
where
|
||||
"deletedAt" < now() - interval '1 millisecond'
|
||||
"userId" = $1
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getPartnerUpserts
|
||||
select
|
||||
"sharedById",
|
||||
"sharedWithId",
|
||||
"inTimeline",
|
||||
"updateId"
|
||||
-- SyncRepository.album.getUpserts
|
||||
select distinct
|
||||
on ("albums"."id", "albums"."updateId") "albums"."id",
|
||||
"albums"."ownerId",
|
||||
"albums"."albumName" as "name",
|
||||
"albums"."description",
|
||||
"albums"."createdAt",
|
||||
"albums"."updatedAt",
|
||||
"albums"."albumThumbnailAssetId" as "thumbnailAssetId",
|
||||
"albums"."isActivityEnabled",
|
||||
"albums"."order",
|
||||
"albums"."updateId"
|
||||
from
|
||||
"partners"
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "album_users" on "albums"."id" = "album_users"."albumsId"
|
||||
where
|
||||
(
|
||||
"sharedById" = $1
|
||||
or "sharedWithId" = $2
|
||||
"albums"."updatedAt" < now() - interval '1 millisecond'
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
"albums"."updateId" asc
|
||||
|
||||
-- SyncRepository.getPartnerDeletes
|
||||
-- SyncRepository.albumAsset.getBackfill
|
||||
select
|
||||
"assets"."id",
|
||||
"assets"."ownerId",
|
||||
"assets"."originalFileName",
|
||||
"assets"."thumbhash",
|
||||
"assets"."checksum",
|
||||
"assets"."fileCreatedAt",
|
||||
"assets"."fileModifiedAt",
|
||||
"assets"."localDateTime",
|
||||
"assets"."type",
|
||||
"assets"."deletedAt",
|
||||
"assets"."isFavorite",
|
||||
"assets"."visibility",
|
||||
"assets"."duration",
|
||||
"assets"."updateId"
|
||||
from
|
||||
"assets"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
||||
where
|
||||
"album_assets"."albumsId" = $1
|
||||
and "assets"."updatedAt" < now() - interval '1 millisecond'
|
||||
and "assets"."updateId" <= $2
|
||||
and "assets"."updateId" >= $3
|
||||
order by
|
||||
"assets"."updateId" asc
|
||||
|
||||
-- SyncRepository.albumAsset.getUpserts
|
||||
select
|
||||
"assets"."id",
|
||||
"assets"."ownerId",
|
||||
"assets"."originalFileName",
|
||||
"assets"."thumbhash",
|
||||
"assets"."checksum",
|
||||
"assets"."fileCreatedAt",
|
||||
"assets"."fileModifiedAt",
|
||||
"assets"."localDateTime",
|
||||
"assets"."type",
|
||||
"assets"."deletedAt",
|
||||
"assets"."isFavorite",
|
||||
"assets"."visibility",
|
||||
"assets"."duration",
|
||||
"assets"."updateId"
|
||||
from
|
||||
"assets"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
||||
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||
where
|
||||
"assets"."updatedAt" < now() - interval '1 millisecond'
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
order by
|
||||
"assets"."updateId" asc
|
||||
|
||||
-- SyncRepository.albumAssetExif.getBackfill
|
||||
select
|
||||
"exif"."assetId",
|
||||
"exif"."description",
|
||||
"exif"."exifImageWidth",
|
||||
"exif"."exifImageHeight",
|
||||
"exif"."fileSizeInByte",
|
||||
"exif"."orientation",
|
||||
"exif"."dateTimeOriginal",
|
||||
"exif"."modifyDate",
|
||||
"exif"."timeZone",
|
||||
"exif"."latitude",
|
||||
"exif"."longitude",
|
||||
"exif"."projectionType",
|
||||
"exif"."city",
|
||||
"exif"."state",
|
||||
"exif"."country",
|
||||
"exif"."make",
|
||||
"exif"."model",
|
||||
"exif"."lensModel",
|
||||
"exif"."fNumber",
|
||||
"exif"."focalLength",
|
||||
"exif"."iso",
|
||||
"exif"."exposureTime",
|
||||
"exif"."profileDescription",
|
||||
"exif"."rating",
|
||||
"exif"."fps",
|
||||
"exif"."updateId"
|
||||
from
|
||||
"exif"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId"
|
||||
where
|
||||
"album_assets"."albumsId" = $1
|
||||
and "exif"."updatedAt" < now() - interval '1 millisecond'
|
||||
and "exif"."updateId" <= $2
|
||||
and "exif"."updateId" >= $3
|
||||
order by
|
||||
"exif"."updateId" asc
|
||||
|
||||
-- SyncRepository.albumAssetExif.getUpserts
|
||||
select
|
||||
"exif"."assetId",
|
||||
"exif"."description",
|
||||
"exif"."exifImageWidth",
|
||||
"exif"."exifImageHeight",
|
||||
"exif"."fileSizeInByte",
|
||||
"exif"."orientation",
|
||||
"exif"."dateTimeOriginal",
|
||||
"exif"."modifyDate",
|
||||
"exif"."timeZone",
|
||||
"exif"."latitude",
|
||||
"exif"."longitude",
|
||||
"exif"."projectionType",
|
||||
"exif"."city",
|
||||
"exif"."state",
|
||||
"exif"."country",
|
||||
"exif"."make",
|
||||
"exif"."model",
|
||||
"exif"."lensModel",
|
||||
"exif"."fNumber",
|
||||
"exif"."focalLength",
|
||||
"exif"."iso",
|
||||
"exif"."exposureTime",
|
||||
"exif"."profileDescription",
|
||||
"exif"."rating",
|
||||
"exif"."fps",
|
||||
"exif"."updateId"
|
||||
from
|
||||
"exif"
|
||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId"
|
||||
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||
where
|
||||
"exif"."updatedAt" < now() - interval '1 millisecond'
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
order by
|
||||
"exif"."updateId" asc
|
||||
|
||||
-- SyncRepository.albumToAsset.getBackfill
|
||||
select
|
||||
"album_assets"."assetsId" as "assetId",
|
||||
"album_assets"."albumsId" as "albumId",
|
||||
"album_assets"."updateId"
|
||||
from
|
||||
"albums_assets_assets" as "album_assets"
|
||||
where
|
||||
"album_assets"."albumsId" = $1
|
||||
and "album_assets"."updatedAt" < now() - interval '1 millisecond'
|
||||
and "album_assets"."updateId" <= $2
|
||||
and "album_assets"."updateId" >= $3
|
||||
order by
|
||||
"album_assets"."updateId" asc
|
||||
|
||||
-- SyncRepository.albumToAsset.getDeletes
|
||||
select
|
||||
"id",
|
||||
"sharedById",
|
||||
"sharedWithId"
|
||||
"assetId",
|
||||
"albumId"
|
||||
from
|
||||
"partners_audit"
|
||||
"album_assets_audit"
|
||||
where
|
||||
(
|
||||
"sharedById" = $1
|
||||
or "sharedWithId" = $2
|
||||
"albumId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"ownerId" = $1
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumsId" as "id"
|
||||
from
|
||||
"albums_shared_users_users" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."usersId" = $2
|
||||
)
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getAssetUpserts
|
||||
-- SyncRepository.albumToAsset.getUpserts
|
||||
select
|
||||
"id",
|
||||
"ownerId",
|
||||
"originalFileName",
|
||||
"thumbhash",
|
||||
"checksum",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"localDateTime",
|
||||
"type",
|
||||
"deletedAt",
|
||||
"isFavorite",
|
||||
"visibility",
|
||||
"updateId",
|
||||
"duration"
|
||||
"album_assets"."assetsId" as "assetId",
|
||||
"album_assets"."albumsId" as "albumId",
|
||||
"album_assets"."updateId"
|
||||
from
|
||||
"assets"
|
||||
"albums_assets_assets" as "album_assets"
|
||||
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||
where
|
||||
"ownerId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
"album_assets"."updatedAt" < now() - interval '1 millisecond'
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
order by
|
||||
"updateId" asc
|
||||
"album_assets"."updateId" asc
|
||||
|
||||
-- SyncRepository.getPartnerBackfill
|
||||
-- SyncRepository.albumUser.getBackfill
|
||||
select
|
||||
"sharedById",
|
||||
"createId"
|
||||
"album_users"."albumsId" as "albumId",
|
||||
"album_users"."usersId" as "userId",
|
||||
"album_users"."role",
|
||||
"album_users"."updateId"
|
||||
from
|
||||
"partners"
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
and "createId" >= $2
|
||||
and "createdAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"partners"."createId" asc
|
||||
|
||||
-- SyncRepository.getPartnerAssetsBackfill
|
||||
select
|
||||
"id",
|
||||
"ownerId",
|
||||
"originalFileName",
|
||||
"thumbhash",
|
||||
"checksum",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"localDateTime",
|
||||
"type",
|
||||
"deletedAt",
|
||||
"isFavorite",
|
||||
"visibility",
|
||||
"updateId",
|
||||
"duration"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"ownerId" = $1
|
||||
"albumsId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
and "updateId" <= $2
|
||||
and "updateId" >= $3
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getPartnerAssetsUpserts
|
||||
-- SyncRepository.albumUser.getDeletes
|
||||
select
|
||||
"id",
|
||||
"ownerId",
|
||||
"originalFileName",
|
||||
"thumbhash",
|
||||
"checksum",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"localDateTime",
|
||||
"type",
|
||||
"deletedAt",
|
||||
"isFavorite",
|
||||
"visibility",
|
||||
"updateId",
|
||||
"duration"
|
||||
"userId",
|
||||
"albumId"
|
||||
from
|
||||
"assets"
|
||||
"album_users_audit"
|
||||
where
|
||||
"ownerId" in (
|
||||
"albumId" in (
|
||||
select
|
||||
"sharedById"
|
||||
"id"
|
||||
from
|
||||
"partners"
|
||||
"albums"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
"ownerId" = $1
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumsId" as "id"
|
||||
from
|
||||
"albums_shared_users_users" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."usersId" = $2
|
||||
)
|
||||
)
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getAssetDeletes
|
||||
-- SyncRepository.albumUser.getUpserts
|
||||
select
|
||||
"album_users"."albumsId" as "albumId",
|
||||
"album_users"."usersId" as "userId",
|
||||
"album_users"."role",
|
||||
"album_users"."updateId"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."updatedAt" < now() - interval '1 millisecond'
|
||||
and "album_users"."albumsId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"ownerId" = $1
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumsId" as "id"
|
||||
from
|
||||
"albums_shared_users_users" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."usersId" = $2
|
||||
)
|
||||
)
|
||||
order by
|
||||
"album_users"."updateId" asc
|
||||
|
||||
-- SyncRepository.asset.getDeletes
|
||||
select
|
||||
"id",
|
||||
"assetId"
|
||||
@@ -178,26 +336,31 @@ where
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getPartnerAssetDeletes
|
||||
-- SyncRepository.asset.getUpserts
|
||||
select
|
||||
"id",
|
||||
"assetId"
|
||||
"assets"."id",
|
||||
"assets"."ownerId",
|
||||
"assets"."originalFileName",
|
||||
"assets"."thumbhash",
|
||||
"assets"."checksum",
|
||||
"assets"."fileCreatedAt",
|
||||
"assets"."fileModifiedAt",
|
||||
"assets"."localDateTime",
|
||||
"assets"."type",
|
||||
"assets"."deletedAt",
|
||||
"assets"."isFavorite",
|
||||
"assets"."visibility",
|
||||
"assets"."duration",
|
||||
"assets"."updateId"
|
||||
from
|
||||
"assets_audit"
|
||||
"assets"
|
||||
where
|
||||
"ownerId" in (
|
||||
select
|
||||
"sharedById"
|
||||
from
|
||||
"partners"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
"ownerId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getAssetExifsUpserts
|
||||
-- SyncRepository.assetExif.getUpserts
|
||||
select
|
||||
"exif"."assetId",
|
||||
"exif"."description",
|
||||
@@ -240,7 +403,204 @@ where
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getPartnerAssetExifsBackfill
|
||||
-- SyncRepository.memory.getDeletes
|
||||
select
|
||||
"id",
|
||||
"memoryId"
|
||||
from
|
||||
"memories_audit"
|
||||
where
|
||||
"userId" = $1
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.memory.getUpserts
|
||||
select
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"deletedAt",
|
||||
"ownerId",
|
||||
"type",
|
||||
"data",
|
||||
"isSaved",
|
||||
"memoryAt",
|
||||
"seenAt",
|
||||
"showAt",
|
||||
"hideAt",
|
||||
"updateId"
|
||||
from
|
||||
"memories"
|
||||
where
|
||||
"ownerId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.memoryToAsset.getDeletes
|
||||
select
|
||||
"id",
|
||||
"memoryId",
|
||||
"assetId"
|
||||
from
|
||||
"memory_assets_audit"
|
||||
where
|
||||
"memoryId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"memories"
|
||||
where
|
||||
"ownerId" = $1
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.memoryToAsset.getUpserts
|
||||
select
|
||||
"memoriesId" as "memoryId",
|
||||
"assetsId" as "assetId",
|
||||
"updateId"
|
||||
from
|
||||
"memories_assets_assets"
|
||||
where
|
||||
"memoriesId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"memories"
|
||||
where
|
||||
"ownerId" = $1
|
||||
)
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.partner.getCreatedAfter
|
||||
select
|
||||
"sharedById",
|
||||
"createId"
|
||||
from
|
||||
"partners"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
and "createId" >= $2
|
||||
and "createdAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"partners"."createId" asc
|
||||
|
||||
-- SyncRepository.partner.getDeletes
|
||||
select
|
||||
"id",
|
||||
"sharedById",
|
||||
"sharedWithId"
|
||||
from
|
||||
"partners_audit"
|
||||
where
|
||||
(
|
||||
"sharedById" = $1
|
||||
or "sharedWithId" = $2
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.partner.getUpserts
|
||||
select
|
||||
"sharedById",
|
||||
"sharedWithId",
|
||||
"inTimeline",
|
||||
"updateId"
|
||||
from
|
||||
"partners"
|
||||
where
|
||||
(
|
||||
"sharedById" = $1
|
||||
or "sharedWithId" = $2
|
||||
)
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.partnerAsset.getBackfill
|
||||
select
|
||||
"assets"."id",
|
||||
"assets"."ownerId",
|
||||
"assets"."originalFileName",
|
||||
"assets"."thumbhash",
|
||||
"assets"."checksum",
|
||||
"assets"."fileCreatedAt",
|
||||
"assets"."fileModifiedAt",
|
||||
"assets"."localDateTime",
|
||||
"assets"."type",
|
||||
"assets"."deletedAt",
|
||||
"assets"."isFavorite",
|
||||
"assets"."visibility",
|
||||
"assets"."duration",
|
||||
"assets"."updateId"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"ownerId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
and "updateId" <= $2
|
||||
and "updateId" >= $3
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.partnerAsset.getDeletes
|
||||
select
|
||||
"id",
|
||||
"assetId"
|
||||
from
|
||||
"assets_audit"
|
||||
where
|
||||
"ownerId" in (
|
||||
select
|
||||
"sharedById"
|
||||
from
|
||||
"partners"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.partnerAsset.getUpserts
|
||||
select
|
||||
"assets"."id",
|
||||
"assets"."ownerId",
|
||||
"assets"."originalFileName",
|
||||
"assets"."thumbhash",
|
||||
"assets"."checksum",
|
||||
"assets"."fileCreatedAt",
|
||||
"assets"."fileModifiedAt",
|
||||
"assets"."localDateTime",
|
||||
"assets"."type",
|
||||
"assets"."deletedAt",
|
||||
"assets"."isFavorite",
|
||||
"assets"."visibility",
|
||||
"assets"."duration",
|
||||
"assets"."updateId"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"ownerId" in (
|
||||
select
|
||||
"sharedById"
|
||||
from
|
||||
"partners"
|
||||
where
|
||||
"sharedWithId" = $1
|
||||
)
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.partnerAssetExif.getBackfill
|
||||
select
|
||||
"exif"."assetId",
|
||||
"exif"."description",
|
||||
@@ -279,7 +639,7 @@ where
|
||||
order by
|
||||
"exif"."updateId" asc
|
||||
|
||||
-- SyncRepository.getPartnerAssetExifsUpserts
|
||||
-- SyncRepository.partnerAssetExif.getUpserts
|
||||
select
|
||||
"exif"."assetId",
|
||||
"exif"."description",
|
||||
@@ -329,126 +689,27 @@ where
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getAlbumDeletes
|
||||
-- SyncRepository.user.getDeletes
|
||||
select
|
||||
"id",
|
||||
"albumId"
|
||||
"userId"
|
||||
from
|
||||
"albums_audit"
|
||||
"users_audit"
|
||||
where
|
||||
"userId" = $1
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
"deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getAlbumUpserts
|
||||
select distinct
|
||||
on ("albums"."id", "albums"."updateId") "albums"."id",
|
||||
"albums"."ownerId",
|
||||
"albums"."albumName" as "name",
|
||||
"albums"."description",
|
||||
"albums"."createdAt",
|
||||
"albums"."updatedAt",
|
||||
"albums"."albumThumbnailAssetId" as "thumbnailAssetId",
|
||||
"albums"."isActivityEnabled",
|
||||
"albums"."order",
|
||||
"albums"."updateId"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "album_users" on "albums"."id" = "album_users"."albumsId"
|
||||
where
|
||||
"albums"."updatedAt" < now() - interval '1 millisecond'
|
||||
and (
|
||||
"albums"."ownerId" = $1
|
||||
or "album_users"."usersId" = $2
|
||||
)
|
||||
order by
|
||||
"albums"."updateId" asc
|
||||
|
||||
-- SyncRepository.getAlbumUserDeletes
|
||||
-- SyncRepository.user.getUpserts
|
||||
select
|
||||
"id",
|
||||
"userId",
|
||||
"albumId"
|
||||
"name",
|
||||
"email",
|
||||
"deletedAt",
|
||||
"updateId"
|
||||
from
|
||||
"album_users_audit"
|
||||
"users"
|
||||
where
|
||||
"albumId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"ownerId" = $1
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumsId" as "id"
|
||||
from
|
||||
"albums_shared_users_users" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."usersId" = $2
|
||||
)
|
||||
)
|
||||
and "deletedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getAlbumBackfill
|
||||
select
|
||||
"albumsId" as "id",
|
||||
"createId"
|
||||
from
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"usersId" = $1
|
||||
and "createId" >= $2
|
||||
and "createdAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"createId" asc
|
||||
|
||||
-- SyncRepository.getAlbumUsersBackfill
|
||||
select
|
||||
"albums_shared_users_users"."albumsId" as "albumId",
|
||||
"albums_shared_users_users"."usersId" as "userId",
|
||||
"albums_shared_users_users"."role",
|
||||
"albums_shared_users_users"."updateId"
|
||||
from
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"albumsId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
and "updateId" <= $2
|
||||
and "updateId" >= $3
|
||||
"updatedAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getAlbumUserUpserts
|
||||
select
|
||||
"albums_shared_users_users"."albumsId" as "albumId",
|
||||
"albums_shared_users_users"."usersId" as "userId",
|
||||
"albums_shared_users_users"."role",
|
||||
"albums_shared_users_users"."updateId"
|
||||
from
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"albums_shared_users_users"."updatedAt" < now() - interval '1 millisecond'
|
||||
and "albums_shared_users_users"."albumsId" in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"ownerId" = $1
|
||||
union
|
||||
(
|
||||
select
|
||||
"albumUsers"."albumsId" as "id"
|
||||
from
|
||||
"albums_shared_users_users" as "albumUsers"
|
||||
where
|
||||
"albumUsers"."usersId" = $2
|
||||
)
|
||||
)
|
||||
order by
|
||||
"albums_shared_users_users"."updateId" asc
|
||||
|
||||
@@ -97,6 +97,16 @@ where
|
||||
"users"."id" = $1
|
||||
and "users"."deletedAt" is null
|
||||
|
||||
-- UserRepository.getForChangePassword
|
||||
select
|
||||
"users"."id",
|
||||
"users"."password"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = $1
|
||||
and "users"."deletedAt" is null
|
||||
|
||||
-- UserRepository.getByEmail
|
||||
select
|
||||
"id",
|
||||
|
||||
@@ -273,8 +273,7 @@ export class AssetJobRepository {
|
||||
.leftJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id')
|
||||
.where((eb) =>
|
||||
eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
|
||||
)
|
||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN),
|
||||
),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.stream();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
@@ -81,6 +82,7 @@ export const repositories = [
|
||||
StackRepository,
|
||||
StorageRepository,
|
||||
SyncRepository,
|
||||
SyncCheckpointRepository,
|
||||
SystemMetadataRepository,
|
||||
TagRepository,
|
||||
TelemetryRepository,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } fr
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
|
||||
import { removeUndefinedKeys } from 'src/utils/database';
|
||||
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
|
||||
@@ -102,6 +102,7 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@Chunked()
|
||||
async delete(ids: string[]): Promise<void> {
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
@@ -517,4 +518,13 @@ export class PersonRepository {
|
||||
await sql`REINDEX TABLE face_search`.execute(this.db);
|
||||
}
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@Chunked()
|
||||
getForPeopleDelete(ids: string[]) {
|
||||
if (ids.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.db.selectFrom('person').select(['id', 'thumbnailPath']).where('id', 'in', ids).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, SessionSyncCheckpoints } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SyncEntityType } from 'src/enum';
|
||||
|
||||
@Injectable()
|
||||
export class SyncCheckpointRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAll(sessionId: string) {
|
||||
return this.db
|
||||
.selectFrom('session_sync_checkpoints')
|
||||
.select(['type', 'ack'])
|
||||
.where('sessionId', '=', sessionId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
upsertAll(items: Insertable<SessionSyncCheckpoints>[]) {
|
||||
return this.db
|
||||
.insertInto('session_sync_checkpoints')
|
||||
.values(items)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['sessionId', 'type']).doUpdateSet((eb) => ({
|
||||
ack: eb.ref('excluded.ack'),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
deleteAll(sessionId: string, types?: SyncEntityType[]) {
|
||||
return this.db
|
||||
.deleteFrom('session_sync_checkpoints')
|
||||
.where('sessionId', '=', sessionId)
|
||||
.$if(!!types, (qb) => qb.where('type', 'in', types!))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
@@ -1,199 +1,104 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, SessionSyncCheckpoints } from 'src/db';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SyncEntityType } from 'src/enum';
|
||||
import { SyncAck } from 'src/types';
|
||||
|
||||
type AuditTables = 'users_audit' | 'partners_audit' | 'assets_audit' | 'albums_audit' | 'album_users_audit';
|
||||
type UpsertTables = 'users' | 'partners' | 'assets' | 'exif' | 'albums' | 'albums_shared_users_users';
|
||||
type AuditTables =
|
||||
| 'users_audit'
|
||||
| 'partners_audit'
|
||||
| 'assets_audit'
|
||||
| 'albums_audit'
|
||||
| 'album_users_audit'
|
||||
| 'album_assets_audit'
|
||||
| 'memories_audit'
|
||||
| 'memory_assets_audit';
|
||||
type UpsertTables =
|
||||
| 'users'
|
||||
| 'partners'
|
||||
| 'assets'
|
||||
| 'exif'
|
||||
| 'albums'
|
||||
| 'albums_shared_users_users'
|
||||
| 'memories'
|
||||
| 'memories_assets_assets';
|
||||
|
||||
@Injectable()
|
||||
export class SyncRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
album: AlbumSync;
|
||||
albumAsset: AlbumAssetSync;
|
||||
albumAssetExif: AlbumAssetExifSync;
|
||||
albumToAsset: AlbumToAssetSync;
|
||||
albumUser: AlbumUserSync;
|
||||
asset: AssetSync;
|
||||
assetExif: AssetExifSync;
|
||||
memory: MemorySync;
|
||||
memoryToAsset: MemoryToAssetSync;
|
||||
partner: PartnerSync;
|
||||
partnerAsset: PartnerAssetsSync;
|
||||
partnerAssetExif: PartnerAssetExifsSync;
|
||||
user: UserSync;
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getCheckpoints(sessionId: string) {
|
||||
return this.db
|
||||
.selectFrom('session_sync_checkpoints')
|
||||
.select(['type', 'ack'])
|
||||
.where('sessionId', '=', sessionId)
|
||||
.execute();
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {
|
||||
this.album = new AlbumSync(this.db);
|
||||
this.albumAsset = new AlbumAssetSync(this.db);
|
||||
this.albumAssetExif = new AlbumAssetExifSync(this.db);
|
||||
this.albumToAsset = new AlbumToAssetSync(this.db);
|
||||
this.albumUser = new AlbumUserSync(this.db);
|
||||
this.asset = new AssetSync(this.db);
|
||||
this.assetExif = new AssetExifSync(this.db);
|
||||
this.memory = new MemorySync(this.db);
|
||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||
this.partner = new PartnerSync(this.db);
|
||||
this.partnerAsset = new PartnerAssetsSync(this.db);
|
||||
this.partnerAssetExif = new PartnerAssetExifsSync(this.db);
|
||||
this.user = new UserSync(this.db);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseSync {
|
||||
constructor(protected db: Kysely<DB>) {}
|
||||
|
||||
protected auditTableFilters<T extends keyof Pick<DB, AuditTables>, D>(
|
||||
qb: SelectQueryBuilder<DB, T, D>,
|
||||
ack?: SyncAck,
|
||||
) {
|
||||
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
||||
return builder
|
||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||
}
|
||||
|
||||
upsertCheckpoints(items: Insertable<SessionSyncCheckpoints>[]) {
|
||||
return this.db
|
||||
.insertInto('session_sync_checkpoints')
|
||||
.values(items)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['sessionId', 'type']).doUpdateSet((eb) => ({
|
||||
ack: eb.ref('excluded.ack'),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
deleteCheckpoints(sessionId: string, types?: SyncEntityType[]) {
|
||||
return this.db
|
||||
.deleteFrom('session_sync_checkpoints')
|
||||
.where('sessionId', '=', sessionId)
|
||||
.$if(!!types, (qb) => qb.where('type', 'in', types!))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getUserUpserts(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getUserDeletes(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('users_audit')
|
||||
.select(['id', 'userId'])
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getPartnerUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('partners')
|
||||
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getPartnerDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('partners_audit')
|
||||
.select(['id', 'sharedById', 'sharedWithId'])
|
||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAssetUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.where('ownerId', '=', userId)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
protected upsertTableFilters<T extends keyof Pick<DB, UpsertTables>, D>(
|
||||
qb: SelectQueryBuilder<DB, T, D>,
|
||||
ack?: SyncAck,
|
||||
) {
|
||||
const builder = qb as SelectQueryBuilder<DB, UpsertTables, D>;
|
||||
return builder
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
||||
.orderBy('updateId', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getPartnerBackfill(userId: string, afterCreateId?: string) {
|
||||
getCreatedAfter(userId: string, afterCreateId?: string) {
|
||||
return this.db
|
||||
.selectFrom('partners')
|
||||
.select(['sharedById', 'createId'])
|
||||
.where('sharedWithId', '=', userId)
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select(['albumsId as id', 'createId'])
|
||||
.where('usersId', '=', userId)
|
||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.orderBy('partners.createId', 'asc')
|
||||
.orderBy('createId', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getPartnerAssetsBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.where('ownerId', '=', partnerId)
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||
.orderBy('updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getPartnerAssetsUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAssetDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets_audit')
|
||||
.select(['id', 'assetId'])
|
||||
.where('ownerId', '=', userId)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getPartnerAssetDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets_audit')
|
||||
.select(['id', 'assetId'])
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAssetExifsUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId))
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getPartnerAssetExifsBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
||||
.where('assets.ownerId', '=', partnerId)
|
||||
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('exif.updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
|
||||
.orderBy('exif.updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getPartnerAssetExifsUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.where('assetId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.select('id')
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
),
|
||||
)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAlbumDeletes(userId: string, ack?: SyncAck) {
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('albums_audit')
|
||||
.select(['id', 'albumId'])
|
||||
@@ -203,7 +108,7 @@ export class SyncRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAlbumUpserts(userId: string, ack?: SyncAck) {
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.distinctOn(['albums.id', 'albums.updateId'])
|
||||
@@ -226,9 +131,147 @@ export class SyncRepository {
|
||||
])
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumAssetSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||
.select(columns.syncAsset)
|
||||
.select('assets.updateId')
|
||||
.where('album_assets.albumsId', '=', albumId)
|
||||
.where('assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('assets.updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('assets.updateId', '>=', afterUpdateId!))
|
||||
.orderBy('assets.updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAlbumUserDeletes(userId: string, ack?: SyncAck) {
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||
.select(columns.syncAsset)
|
||||
.select('assets.updateId')
|
||||
.where('assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('assets.updateId', '>', ack!.updateId))
|
||||
.orderBy('assets.updateId', 'asc')
|
||||
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumAssetExifSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId')
|
||||
.select(columns.syncAssetExif)
|
||||
.select('exif.updateId')
|
||||
.where('album_assets.albumsId', '=', albumId)
|
||||
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('exif.updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
|
||||
.orderBy('exif.updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId')
|
||||
.select(columns.syncAssetExif)
|
||||
.select('exif.updateId')
|
||||
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('exif.updateId', '>', ack!.updateId))
|
||||
.orderBy('exif.updateId', 'asc')
|
||||
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumToAssetSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_assets_assets as album_assets')
|
||||
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
||||
.where('album_assets.albumsId', '=', albumId)
|
||||
.where('album_assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('album_assets.updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('album_assets.updateId', '>=', afterUpdateId!))
|
||||
.orderBy('album_assets.updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('album_assets_audit')
|
||||
.select(['id', 'assetId', 'albumId'])
|
||||
.where((eb) =>
|
||||
eb(
|
||||
'albumId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('albums')
|
||||
.select(['id'])
|
||||
.where('ownerId', '=', userId)
|
||||
.union((eb) =>
|
||||
eb.parens(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as albumUsers')
|
||||
.select(['albumUsers.albumsId as id'])
|
||||
.where('albumUsers.usersId', '=', userId),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('albums_assets_assets as album_assets')
|
||||
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
||||
.where('album_assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('album_assets.updateId', '>', ack!.updateId))
|
||||
.orderBy('album_assets.updateId', 'asc')
|
||||
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumUserSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.select(columns.syncAlbumUser)
|
||||
.select('album_users.updateId')
|
||||
.where('albumsId', '=', albumId)
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||
.orderBy('updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('album_users_audit')
|
||||
.select(['id', 'userId', 'albumId'])
|
||||
@@ -254,42 +297,18 @@ export class SyncRepository {
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getAlbumBackfill(userId: string, afterCreateId?: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select(['albumsId as id', 'createId'])
|
||||
.where('usersId', '=', userId)
|
||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.orderBy('createId', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getAlbumUsersBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select(columns.syncAlbumUser)
|
||||
.where('albumsId', '=', albumId)
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||
.orderBy('updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.select(columns.syncAlbumUser)
|
||||
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
||||
.orderBy('albums_shared_users_users.updateId', 'asc')
|
||||
.select('album_users.updateId')
|
||||
.where('album_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('album_users.updateId', '>', ack!.updateId))
|
||||
.orderBy('album_users.updateId', 'asc')
|
||||
.where((eb) =>
|
||||
eb(
|
||||
'albums_shared_users_users.albumsId',
|
||||
'album_users.albumsId',
|
||||
'in',
|
||||
eb
|
||||
.selectFrom('albums')
|
||||
@@ -307,23 +326,229 @@ export class SyncRepository {
|
||||
)
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
private auditTableFilters<T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
||||
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
||||
return builder
|
||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||
class AssetSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets_audit')
|
||||
.select(['id', 'assetId'])
|
||||
.where('ownerId', '=', userId)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
private upsertTableFilters<T extends keyof Pick<DB, UpsertTables>, D>(
|
||||
qb: SelectQueryBuilder<DB, T, D>,
|
||||
ack?: SyncAck,
|
||||
) {
|
||||
const builder = qb as SelectQueryBuilder<DB, UpsertTables, D>;
|
||||
return builder
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
||||
.orderBy('updateId', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.select('assets.updateId')
|
||||
.where('ownerId', '=', userId)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class AssetExifSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.select('exif.updateId')
|
||||
.where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId))
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class MemorySync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('memories_audit')
|
||||
.select(['id', 'memoryId'])
|
||||
.where('userId', '=', userId)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('memories')
|
||||
.select([
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'ownerId',
|
||||
'type',
|
||||
'data',
|
||||
'isSaved',
|
||||
'memoryAt',
|
||||
'seenAt',
|
||||
'showAt',
|
||||
'hideAt',
|
||||
])
|
||||
.select('updateId')
|
||||
.where('ownerId', '=', userId)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryToAssetSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('memory_assets_audit')
|
||||
.select(['id', 'memoryId', 'assetId'])
|
||||
.where('memoryId', 'in', (eb) => eb.selectFrom('memories').select('id').where('ownerId', '=', userId))
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('memories_assets_assets')
|
||||
.select(['memoriesId as memoryId', 'assetsId as assetId'])
|
||||
.select('updateId')
|
||||
.where('memoriesId', 'in', (eb) => eb.selectFrom('memories').select('id').where('ownerId', '=', userId))
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getCreatedAfter(userId: string, afterCreateId?: string) {
|
||||
return this.db
|
||||
.selectFrom('partners')
|
||||
.select(['sharedById', 'createId'])
|
||||
.where('sharedWithId', '=', userId)
|
||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.orderBy('partners.createId', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('partners_audit')
|
||||
.select(['id', 'sharedById', 'sharedWithId'])
|
||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('partners')
|
||||
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerAssetsSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.select('assets.updateId')
|
||||
.where('ownerId', '=', partnerId)
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||
.orderBy('updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getDeletes(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets_audit')
|
||||
.select(['id', 'assetId'])
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
)
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(columns.syncAsset)
|
||||
.select('assets.updateId')
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class PartnerAssetExifsSync extends BaseSync {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getBackfill(partnerId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.select('exif.updateId')
|
||||
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
||||
.where('assets.ownerId', '=', partnerId)
|
||||
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('exif.updateId', '<=', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
|
||||
.orderBy('exif.updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('exif')
|
||||
.select(columns.syncAssetExif)
|
||||
.select('exif.updateId')
|
||||
.where('assetId', 'in', (eb) =>
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.select('id')
|
||||
.where('ownerId', 'in', (eb) =>
|
||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||
),
|
||||
)
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
class UserSync extends BaseSync {
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getDeletes(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('users_audit')
|
||||
.select(['id', 'userId'])
|
||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
getUpserts(ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
|
||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||
.stream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,16 @@ export class UserRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getForChangePassword(id: string) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.password'])
|
||||
.where('users.id', '=', id)
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.EMAIL] })
|
||||
getByEmail(email: string, options?: { withPassword?: boolean }) {
|
||||
return this.db
|
||||
|
||||
@@ -22,12 +22,12 @@ export class ViewRepository {
|
||||
.where('localDateTime', 'is not', null)
|
||||
.execute();
|
||||
|
||||
return results.map((row) => row.directoryPath.replaceAll(/^\/|\/$/g, ''));
|
||||
return results.map((row) => row.directoryPath.replaceAll(/\/$/g, ''));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string) {
|
||||
const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
|
||||
const normalizedPath = partialPath.replaceAll(/\/$/g, '');
|
||||
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
|
||||
@@ -142,6 +142,20 @@ export const albums_delete_audit = registerFunction({
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const album_assets_delete_audit = registerFunction({
|
||||
name: 'album_assets_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO album_assets_audit ("albumId", "assetId")
|
||||
SELECT "albumsId", "assetsId" FROM OLD
|
||||
WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD));
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const album_users_delete_audit = registerFunction({
|
||||
name: 'album_users_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
@@ -162,3 +176,31 @@ export const album_users_delete_audit = registerFunction({
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const memories_delete_audit = registerFunction({
|
||||
name: 'memories_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO memories_audit ("memoryId", "userId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const memory_assets_delete_audit = registerFunction({
|
||||
name: 'memory_assets_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO memory_assets_audit ("memoryId", "assetId")
|
||||
SELECT "memoriesId", "assetsId" FROM OLD
|
||||
WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD));
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
@@ -8,11 +8,14 @@ import {
|
||||
f_unaccent,
|
||||
immich_uuid_v7,
|
||||
ll_to_earth_public,
|
||||
memories_delete_audit,
|
||||
memory_assets_delete_audit,
|
||||
partners_delete_audit,
|
||||
updated_at,
|
||||
users_delete_audit,
|
||||
} from 'src/schema/functions';
|
||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||
import { AlbumAssetAuditTable } from 'src/schema/tables/album-asset-audit.table';
|
||||
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
||||
import { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
|
||||
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
|
||||
@@ -29,8 +32,10 @@ import { ExifTable } from 'src/schema/tables/exif.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
|
||||
import { MemoryAuditTable } from 'src/schema/tables/memory-audit.table';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||
import { MoveTable } from 'src/schema/tables/move.table';
|
||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||
import { NotificationTable } from 'src/schema/tables/notification.table';
|
||||
@@ -58,6 +63,7 @@ export class ImmichDatabase {
|
||||
tables = [
|
||||
ActivityTable,
|
||||
AlbumAssetTable,
|
||||
AlbumAssetAuditTable,
|
||||
AlbumAuditTable,
|
||||
AlbumUserAuditTable,
|
||||
AlbumUserTable,
|
||||
@@ -73,8 +79,10 @@ export class ImmichDatabase {
|
||||
FaceSearchTable,
|
||||
GeodataPlacesTable,
|
||||
LibraryTable,
|
||||
MemoryAssetTable,
|
||||
MemoryTable,
|
||||
MemoryAuditTable,
|
||||
MemoryAssetTable,
|
||||
MemoryAssetAuditTable,
|
||||
MoveTable,
|
||||
NaturalEarthCountriesTable,
|
||||
NotificationTable,
|
||||
@@ -108,6 +116,8 @@ export class ImmichDatabase {
|
||||
albums_delete_audit,
|
||||
album_user_after_insert,
|
||||
album_users_delete_audit,
|
||||
memories_delete_audit,
|
||||
memory_assets_delete_audit,
|
||||
];
|
||||
|
||||
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "albums_assets_assets" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`ALTER TABLE "albums_assets_assets" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_assets_update_id" ON "albums_assets_assets" ("updateId")`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_assets_updated_at"
|
||||
BEFORE UPDATE ON "albums_assets_assets"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP INDEX "IDX_album_assets_update_id";`.execute(db);
|
||||
await sql`ALTER TABLE "albums_assets_assets" DROP COLUMN "updatedAt";`.execute(db);
|
||||
await sql`ALTER TABLE "albums_assets_assets" DROP COLUMN "updateId";`.execute(db);
|
||||
await sql`DROP TRIGGER "album_assets_updated_at" ON "albums_assets_assets";`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TABLE "album_assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "assetId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||
await sql`ALTER TABLE "album_assets_audit" ADD CONSTRAINT "PK_32969b576ec8f78d84f37c2eb2d" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_assets_audit_album_id" ON "album_assets_audit" ("albumId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_assets_audit_asset_id" ON "album_assets_audit" ("assetId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_assets_audit_deleted_at" ON "album_assets_audit" ("deletedAt")`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_assets_updated_at"
|
||||
BEFORE UPDATE ON "albums_assets_assets"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "album_assets_updated_at" ON "albums_assets_assets";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_assets_audit_album_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_assets_audit_asset_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_assets_audit_deleted_at";`.execute(db);
|
||||
await sql`ALTER TABLE "album_assets_audit" DROP CONSTRAINT "PK_32969b576ec8f78d84f37c2eb2d";`.execute(db);
|
||||
await sql`DROP TABLE "album_assets_audit";`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION album_assets_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO album_assets_audit ("albumId", "assetId")
|
||||
SELECT "albumsId", "assetsId" FROM OLD
|
||||
WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD));
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`ALTER TABLE "album_assets_audit" ADD CONSTRAINT "FK_8047b44b812619a3c75a2839b0d" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_assets_delete_audit"
|
||||
AFTER DELETE ON "albums_assets_assets"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() <= 1)
|
||||
EXECUTE FUNCTION album_assets_delete_audit();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "album_assets_delete_audit" ON "albums_assets_assets";`.execute(db);
|
||||
await sql`ALTER TABLE "album_assets_audit" DROP CONSTRAINT "FK_8047b44b812619a3c75a2839b0d";`.execute(db);
|
||||
await sql`DROP FUNCTION album_assets_delete_audit;`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TABLE "memory_assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "memoryId" uuid NOT NULL, "assetId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||
await sql`CREATE TABLE "memories_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "memoryId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" ADD "createdAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||
await sql`ALTER TABLE "memory_assets_audit" ADD CONSTRAINT "PK_35ef16910228f980e0766dcc59b" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`ALTER TABLE "memories_audit" ADD CONSTRAINT "PK_19de798c033a710dcfa5c72f81b" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`ALTER TABLE "memory_assets_audit" ADD CONSTRAINT "FK_225a204afcb0bd6de015080fb03" FOREIGN KEY ("memoryId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memory_assets_audit_memory_id" ON "memory_assets_audit" ("memoryId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memory_assets_audit_asset_id" ON "memory_assets_audit" ("assetId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memory_assets_audit_deleted_at" ON "memory_assets_audit" ("deletedAt")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memory_assets_update_id" ON "memories_assets_assets" ("updateId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memories_audit_memory_id" ON "memories_audit" ("memoryId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memories_audit_user_id" ON "memories_audit" ("userId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_memories_audit_deleted_at" ON "memories_audit" ("deletedAt")`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION memories_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO memories_audit ("memoryId", "userId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION memory_assets_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO memory_assets_audit ("memoryId", "assetId")
|
||||
SELECT "memoriesId", "assetsId" FROM OLD
|
||||
WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD));
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "memories_delete_audit"
|
||||
AFTER DELETE ON "memories"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() = 0)
|
||||
EXECUTE FUNCTION memories_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "memory_assets_delete_audit"
|
||||
AFTER DELETE ON "memories_assets_assets"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() <= 1)
|
||||
EXECUTE FUNCTION memory_assets_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "memory_assets_updated_at"
|
||||
BEFORE UPDATE ON "memories_assets_assets"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "memories_delete_audit" ON "memories";`.execute(db);
|
||||
await sql`DROP TRIGGER "memory_assets_delete_audit" ON "memories_assets_assets";`.execute(db);
|
||||
await sql`DROP TRIGGER "memory_assets_updated_at" ON "memories_assets_assets";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memory_assets_update_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memory_assets_audit_memory_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memory_assets_audit_asset_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memory_assets_audit_deleted_at";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memories_audit_memory_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memories_audit_user_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_memories_audit_deleted_at";`.execute(db);
|
||||
await sql`ALTER TABLE "memory_assets_audit" DROP CONSTRAINT "FK_225a204afcb0bd6de015080fb03";`.execute(db);
|
||||
await sql`ALTER TABLE "memory_assets_audit" DROP CONSTRAINT "PK_35ef16910228f980e0766dcc59b";`.execute(db);
|
||||
await sql`ALTER TABLE "memories_audit" DROP CONSTRAINT "PK_19de798c033a710dcfa5c72f81b";`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "createdAt";`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "updatedAt";`.execute(db);
|
||||
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "updateId";`.execute(db);
|
||||
await sql`DROP TABLE "memory_assets_audit";`.execute(db);
|
||||
await sql`DROP TABLE "memories_audit";`.execute(db);
|
||||
await sql`DROP FUNCTION memories_delete_audit;`.execute(db);
|
||||
await sql`DROP FUNCTION memory_assets_delete_audit;`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('album_assets_audit')
|
||||
export class AlbumAssetAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: string;
|
||||
|
||||
@ForeignKeyColumn(() => AlbumTable, {
|
||||
type: 'uuid',
|
||||
indexName: 'IDX_album_assets_audit_album_id',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
})
|
||||
albumId!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_album_assets_audit_asset_id' })
|
||||
assetId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_assets_audit_deleted_at' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { album_assets_delete_audit } from 'src/schema/functions';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||
import { AfterDeleteTrigger, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||
|
||||
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
||||
@UpdatedAtTrigger('album_assets_updated_at')
|
||||
@AfterDeleteTrigger({
|
||||
name: 'album_assets_delete_audit',
|
||||
scope: 'statement',
|
||||
function: album_assets_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() <= 1',
|
||||
})
|
||||
export class AlbumAssetTable {
|
||||
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true })
|
||||
albumsId!: string;
|
||||
@@ -12,4 +22,10 @@ export class AlbumAssetTable {
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@UpdateIdColumn({ indexName: 'IDX_album_assets_update_id' })
|
||||
updateId!: string;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import 'src/schema/tables/exif.table';
|
||||
import 'src/schema/tables/face-search.table';
|
||||
import 'src/schema/tables/geodata-places.table';
|
||||
import 'src/schema/tables/library.table';
|
||||
import 'src/schema/tables/memory-asset.table';
|
||||
import 'src/schema/tables/memory.table';
|
||||
import 'src/schema/tables/memory_asset.table';
|
||||
import 'src/schema/tables/move.table';
|
||||
import 'src/schema/tables/natural-earth-countries.table';
|
||||
import 'src/schema/tables/partner-audit.table';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('memory_assets_audit')
|
||||
export class MemoryAssetAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: string;
|
||||
|
||||
@ForeignKeyColumn(() => MemoryTable, {
|
||||
type: 'uuid',
|
||||
indexName: 'IDX_memory_assets_audit_memory_id',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
})
|
||||
memoryId!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_memory_assets_audit_asset_id' })
|
||||
assetId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memory_assets_audit_deleted_at' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ColumnType } from 'kysely';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { memory_assets_delete_audit } from 'src/schema/functions';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { AfterDeleteTrigger, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||
|
||||
type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
type Generated<T> =
|
||||
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
||||
|
||||
@Table('memories_assets_assets')
|
||||
@UpdatedAtTrigger('memory_assets_updated_at')
|
||||
@AfterDeleteTrigger({
|
||||
name: 'memory_assets_delete_audit',
|
||||
scope: 'statement',
|
||||
function: memory_assets_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() <= 1',
|
||||
})
|
||||
export class MemoryAssetTable {
|
||||
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||
memoriesId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||
assetsId!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ indexName: 'IDX_memory_assets_update_id' })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ColumnType } from 'kysely';
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||
|
||||
type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
type Generated<T> =
|
||||
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
||||
|
||||
@Table('memories_audit')
|
||||
export class MemoryAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_memories_audit_memory_id' })
|
||||
memoryId!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_memories_audit_user_id' })
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memories_audit_deleted_at' })
|
||||
deletedAt!: Timestamp;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { MemoryType } from 'src/enum';
|
||||
import { memories_delete_audit } from 'src/schema/functions';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import {
|
||||
AfterDeleteTrigger,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
@@ -10,11 +12,17 @@ import {
|
||||
Table,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
import { MemoryData } from 'src/types';
|
||||
|
||||
@Table('memories')
|
||||
@UpdatedAtTrigger('memories_updated_at')
|
||||
export class MemoryTable<T extends MemoryType = MemoryType> {
|
||||
@AfterDeleteTrigger({
|
||||
name: 'memories_delete_audit',
|
||||
scope: 'statement',
|
||||
function: memories_delete_audit,
|
||||
referencingOldTableAs: 'old',
|
||||
when: 'pg_trigger_depth() = 0',
|
||||
})
|
||||
export class MemoryTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: string;
|
||||
|
||||
@@ -31,10 +39,10 @@ export class MemoryTable<T extends MemoryType = MemoryType> {
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
type!: T;
|
||||
type!: MemoryType;
|
||||
|
||||
@Column({ type: 'jsonb' })
|
||||
data!: MemoryData[T];
|
||||
data!: object;
|
||||
|
||||
/** unless set to true, will be automatically deleted in the future */
|
||||
@Column({ type: 'boolean', default: false })
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('memories_assets_assets')
|
||||
export class MemoryAssetTable {
|
||||
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||
memoriesId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||
assetsId!: string;
|
||||
}
|
||||
@@ -78,36 +78,24 @@ export class ApiService {
|
||||
return next();
|
||||
}
|
||||
|
||||
const targets = [
|
||||
{
|
||||
regex: /^\/share\/(.+)$/,
|
||||
onMatch: async (matches: RegExpMatchArray) => {
|
||||
const key = matches[1];
|
||||
const auth = await this.authService.validateSharedLink(key);
|
||||
return this.sharedLinkService.getMetadataTags(auth);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let status = 200;
|
||||
let html = index;
|
||||
|
||||
try {
|
||||
for (const { regex, onMatch } of targets) {
|
||||
const matches = request.url.match(regex);
|
||||
if (matches) {
|
||||
const meta = await onMatch(matches);
|
||||
if (meta) {
|
||||
html = render(index, meta);
|
||||
}
|
||||
|
||||
break;
|
||||
const shareMatches = request.url.match(/^\/share\/(.+)$/);
|
||||
if (shareMatches) {
|
||||
try {
|
||||
const key = shareMatches[1];
|
||||
const auth = await this.authService.validateSharedLink(key);
|
||||
const meta = await this.sharedLinkService.getMetadataTags(auth);
|
||||
if (meta) {
|
||||
html = render(index, meta);
|
||||
}
|
||||
} catch {
|
||||
status = 404;
|
||||
}
|
||||
} catch {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
res.type('text/html').header('Cache-Control', 'no-store').send(html);
|
||||
res.status(status).type('text/html').header('Cache-Control', 'no-store').send(html);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,46 +116,33 @@ describe(AuthService.name, () => {
|
||||
const auth = factory.auth({ user });
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
mocks.user.getByEmail.mockResolvedValue({ ...user, password: 'hash-password' });
|
||||
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' });
|
||||
mocks.user.update.mockResolvedValue(user);
|
||||
|
||||
await sut.changePassword(auth, dto);
|
||||
|
||||
expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, { withPassword: true });
|
||||
expect(mocks.user.getForChangePassword).toHaveBeenCalledWith(user.id);
|
||||
expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
||||
});
|
||||
|
||||
it('should throw when auth user email is not found', async () => {
|
||||
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||
|
||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should throw when password does not match existing password', async () => {
|
||||
const auth = { user: { email: 'test@imimch.com' } as UserAdmin };
|
||||
const user = factory.user();
|
||||
const auth = factory.auth({ user });
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
mocks.crypto.compareBcrypt.mockReturnValue(false);
|
||||
|
||||
mocks.user.getByEmail.mockResolvedValue({
|
||||
email: 'test@immich.com',
|
||||
password: 'hash-password',
|
||||
} as UserAdmin & { password: string });
|
||||
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' });
|
||||
|
||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw when user does not have a password', async () => {
|
||||
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
||||
const user = factory.user();
|
||||
const auth = factory.auth({ user });
|
||||
const dto = { password: 'old-password', newPassword: 'new-password' };
|
||||
|
||||
mocks.user.getByEmail.mockResolvedValue({
|
||||
email: 'test@immich.com',
|
||||
password: '',
|
||||
} as UserAdmin & { password: string });
|
||||
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: '' });
|
||||
|
||||
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
@@ -91,11 +91,7 @@ export class AuthService extends BaseService {
|
||||
|
||||
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||
const { password, newPassword } = dto;
|
||||
const user = await this.userRepository.getByEmail(auth.user.email, { withPassword: true });
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const user = await this.userRepository.getForChangePassword(auth.user.id);
|
||||
const valid = this.validateSecret(password, user.password);
|
||||
if (!valid) {
|
||||
throw new BadRequestException('Wrong password');
|
||||
|
||||
@@ -41,6 +41,7 @@ import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
@@ -53,6 +54,54 @@ import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
|
||||
export const BASE_SERVICE_DEPENDENCIES = [
|
||||
LoggingRepository,
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumRepository,
|
||||
AlbumUserRepository,
|
||||
ApiKeyRepository,
|
||||
AssetRepository,
|
||||
AssetJobRepository,
|
||||
AuditRepository,
|
||||
ConfigRepository,
|
||||
CronRepository,
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
DownloadRepository,
|
||||
DuplicateRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
MachineLearningRepository,
|
||||
MapRepository,
|
||||
MediaRepository,
|
||||
MemoryRepository,
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
ProcessRepository,
|
||||
SearchRepository,
|
||||
ServerInfoRepository,
|
||||
SessionRepository,
|
||||
SharedLinkRepository,
|
||||
StackRepository,
|
||||
StorageRepository,
|
||||
SyncRepository,
|
||||
SyncCheckpointRepository,
|
||||
SystemMetadataRepository,
|
||||
TagRepository,
|
||||
TelemetryRepository,
|
||||
TrashRepository,
|
||||
UserRepository,
|
||||
VersionHistoryRepository,
|
||||
ViewRepository,
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class BaseService {
|
||||
protected storageCore: StorageCore;
|
||||
@@ -95,6 +144,7 @@ export class BaseService {
|
||||
protected stackRepository: StackRepository,
|
||||
protected storageRepository: StorageRepository,
|
||||
protected syncRepository: SyncRepository,
|
||||
protected syncCheckpointRepository: SyncCheckpointRepository,
|
||||
protected systemMetadataRepository: SystemMetadataRepository,
|
||||
protected tagRepository: TagRepository,
|
||||
protected telemetryRepository: TelemetryRepository,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { OnThisDayData } from 'src/types';
|
||||
import { factory, newUuid, newUuids } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -87,7 +88,7 @@ describe(MemoryService.name, () => {
|
||||
await expect(
|
||||
sut.create(factory.auth({ user: { id: userId } }), {
|
||||
type: memory.type,
|
||||
data: memory.data,
|
||||
data: memory.data as OnThisDayData,
|
||||
memoryAt: memory.memoryAt,
|
||||
isSaved: memory.isSaved,
|
||||
assetIds: [assetId],
|
||||
@@ -117,7 +118,7 @@ describe(MemoryService.name, () => {
|
||||
await expect(
|
||||
sut.create(factory.auth({ user: { id: userId } }), {
|
||||
type: memory.type,
|
||||
data: memory.data,
|
||||
data: memory.data as OnThisDayData,
|
||||
assetIds: memory.assets.map((asset) => asset.id),
|
||||
memoryAt: memory.memoryAt,
|
||||
}),
|
||||
@@ -135,7 +136,11 @@ describe(MemoryService.name, () => {
|
||||
mocks.memory.create.mockResolvedValue(memory);
|
||||
|
||||
await expect(
|
||||
sut.create(factory.auth(), { type: memory.type, data: memory.data, memoryAt: memory.memoryAt }),
|
||||
sut.create(factory.auth(), {
|
||||
type: memory.type,
|
||||
data: memory.data as OnThisDayData,
|
||||
memoryAt: memory.memoryAt,
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defaults } from 'src/config';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
@@ -1639,4 +1639,80 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstDateTime', () => {
|
||||
it('should ignore date-only tags like GPSDateStamp', () => {
|
||||
const tags = {
|
||||
GPSDateStamp: '2023:08:08', // Date-only tag, should be ignored
|
||||
SonyDateTime2: '2023:07:07 07:00:00',
|
||||
};
|
||||
|
||||
const result = firstDateTime(tags);
|
||||
expect(result?.tag).toBe('SonyDateTime2');
|
||||
expect(result?.dateTime?.toDate()?.toISOString()).toBe('2023-07-07T07:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should respect full priority order with all date tags present', () => {
|
||||
const tags = {
|
||||
// SubSec and standard EXIF date tags
|
||||
SubSecDateTimeOriginal: '2023:01:01 01:00:00',
|
||||
SubSecCreateDate: '2023:02:02 02:00:00',
|
||||
SubSecMediaCreateDate: '2023:03:03 03:00:00',
|
||||
DateTimeOriginal: '2023:04:04 04:00:00',
|
||||
CreateDate: '2023:05:05 05:00:00',
|
||||
MediaCreateDate: '2023:06:06 06:00:00',
|
||||
CreationDate: '2023:07:07 07:00:00',
|
||||
DateTimeCreated: '2023:08:08 08:00:00',
|
||||
|
||||
// Additional date tags
|
||||
TimeCreated: '2023:09:09 09:00:00',
|
||||
GPSDateTime: '2023:10:10 10:00:00',
|
||||
DateTimeUTC: '2023:11:11 11:00:00',
|
||||
GPSDateStamp: '2023:12:12', // Date-only tag, should be ignored
|
||||
SonyDateTime2: '2023:13:13 13:00:00',
|
||||
|
||||
// Non-standard tag
|
||||
SourceImageCreateTime: '2023:14:14 14:00:00',
|
||||
};
|
||||
|
||||
const result = firstDateTime(tags);
|
||||
// Should use SubSecDateTimeOriginal as it has highest priority
|
||||
expect(result?.tag).toBe('SubSecDateTimeOriginal');
|
||||
expect(result?.dateTime?.toDate()?.toISOString()).toBe('2023-01-01T01:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle missing SubSec tags and use available date tags', () => {
|
||||
const tags = {
|
||||
// Standard date tags
|
||||
CreationDate: '2023:07:07 07:00:00',
|
||||
DateTimeCreated: '2023:08:08 08:00:00',
|
||||
|
||||
// Additional date tags
|
||||
TimeCreated: '2023:09:09 09:00:00',
|
||||
GPSDateTime: '2023:10:10 10:00:00',
|
||||
DateTimeUTC: '2023:11:11 11:00:00',
|
||||
GPSDateStamp: '2023:12:12', // Date-only tag, should be ignored
|
||||
SonyDateTime2: '2023:13:13 13:00:00',
|
||||
};
|
||||
|
||||
const result = firstDateTime(tags);
|
||||
// Should use CreationDate when available
|
||||
expect(result?.tag).toBe('CreationDate');
|
||||
expect(result?.dateTime?.toDate()?.toISOString()).toBe('2023-07-07T07:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle invalid date formats gracefully', () => {
|
||||
const tags = {
|
||||
TimeCreated: 'invalid-date',
|
||||
GPSDateTime: '2023:10:10 10:00:00',
|
||||
DateTimeUTC: 'also-invalid',
|
||||
SonyDateTime2: '2023:13:13 13:00:00',
|
||||
};
|
||||
|
||||
const result = firstDateTime(tags);
|
||||
// Should skip invalid dates and use the first valid one
|
||||
expect(result?.tag).toBe('GPSDateTime');
|
||||
expect(result?.dateTime?.toDate()?.toISOString()).toBe('2023-10-10T10:00:00.000Z');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ContainerDirectoryItem, Maybe, Tags } from 'exiftool-vendored';
|
||||
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
||||
import { ContainerDirectoryItem, ExifDateTime, Tags } from 'exiftool-vendored';
|
||||
import { Insertable } from 'kysely';
|
||||
import _ from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
@@ -32,19 +31,47 @@ import { isFaceImportEnabled } from 'src/utils/misc';
|
||||
import { upsertTags } from 'src/utils/tag';
|
||||
|
||||
/** look for a date from these tags (in order) */
|
||||
const EXIF_DATE_TAGS: Array<keyof Tags> = [
|
||||
const EXIF_DATE_TAGS: Array<keyof ImmichTags> = [
|
||||
'SubSecDateTimeOriginal',
|
||||
'DateTimeOriginal',
|
||||
'SubSecCreateDate',
|
||||
'CreationDate',
|
||||
'CreateDate',
|
||||
'SubSecMediaCreateDate',
|
||||
'DateTimeOriginal',
|
||||
'CreateDate',
|
||||
'MediaCreateDate',
|
||||
'CreationDate',
|
||||
'DateTimeCreated',
|
||||
'GPSDateTime',
|
||||
'DateTimeUTC',
|
||||
'SonyDateTime2',
|
||||
// Undocumented, non-standard tag from insta360 in xmp.GPano namespace
|
||||
'SourceImageCreateTime' as keyof Tags,
|
||||
'SourceImageCreateTime' as keyof ImmichTags,
|
||||
];
|
||||
|
||||
export function firstDateTime(tags: ImmichTags) {
|
||||
for (const tag of EXIF_DATE_TAGS) {
|
||||
const tagValue = tags?.[tag];
|
||||
|
||||
if (tagValue instanceof ExifDateTime) {
|
||||
return {
|
||||
tag,
|
||||
dateTime: tagValue,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof tagValue !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exifDateTime = ExifDateTime.fromEXIF(tagValue);
|
||||
if (exifDateTime) {
|
||||
return {
|
||||
tag,
|
||||
dateTime: exifDateTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validate = <T>(value: T): NonNullable<T> | null => {
|
||||
// handle lists of numbers
|
||||
if (Array.isArray(value)) {
|
||||
@@ -407,7 +434,8 @@ export class MetadataService extends BaseService {
|
||||
|
||||
// prefer dates from sidecar tags
|
||||
if (sidecarTags) {
|
||||
const sidecarDate = firstDateTime(sidecarTags as Tags, EXIF_DATE_TAGS);
|
||||
const result = firstDateTime(sidecarTags);
|
||||
const sidecarDate = result?.dateTime;
|
||||
if (sidecarDate) {
|
||||
for (const tag of EXIF_DATE_TAGS) {
|
||||
delete mediaTags[tag];
|
||||
@@ -748,8 +776,12 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
private getDates(asset: { id: string; originalPath: string }, exifTags: ImmichTags, stats: Stats) {
|
||||
const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
|
||||
this.logger.verbose(`Date and time is ${dateTime} for asset ${asset.id}: ${asset.originalPath}`);
|
||||
const result = firstDateTime(exifTags);
|
||||
const tag = result?.tag;
|
||||
const dateTime = result?.dateTime;
|
||||
this.logger.verbose(
|
||||
`Date and time is ${dateTime} using exifTag ${tag} for asset ${asset.id}: ${asset.originalPath}`,
|
||||
);
|
||||
|
||||
// timezone
|
||||
let timeZone = exifTags.tz ?? null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { Person } from 'src/database';
|
||||
import { AssetFaces, FaceSearch } from 'src/db';
|
||||
import { Chunked, OnJob } from 'src/decorators';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
AssetFaceCreateDto,
|
||||
@@ -216,6 +216,10 @@ export class PersonService extends BaseService {
|
||||
return mapPerson(person);
|
||||
}
|
||||
|
||||
delete(auth: AuthDto, id: string): Promise<void> {
|
||||
return this.deleteAll(auth, { ids: [id] });
|
||||
}
|
||||
|
||||
async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const person of dto.people) {
|
||||
@@ -236,8 +240,14 @@ export class PersonService extends BaseService {
|
||||
return results;
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, { ids }: BulkIdsDto): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_DELETE, ids });
|
||||
const people = await this.personRepository.getForPeopleDelete(ids);
|
||||
await this.removeAllPeople(people);
|
||||
}
|
||||
|
||||
@Chunked()
|
||||
private async delete(people: { id: string; thumbnailPath: string }[]) {
|
||||
private async removeAllPeople(people: { id: string; thumbnailPath: string }[]) {
|
||||
await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath)));
|
||||
await this.personRepository.delete(people.map((person) => person.id));
|
||||
this.logger.debug(`Deleted ${people.length} people`);
|
||||
@@ -246,7 +256,7 @@ export class PersonService extends BaseService {
|
||||
@OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||
async handlePersonCleanup(): Promise<JobStatus> {
|
||||
const people = await this.personRepository.getAllWithoutFaces();
|
||||
await this.delete(people);
|
||||
await this.removeAllPeople(people);
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@@ -589,7 +599,7 @@ export class PersonService extends BaseService {
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
||||
await this.personRepository.reassignFaces(mergeData);
|
||||
await this.delete([mergePerson]);
|
||||
await this.removeAllPeople([mergePerson]);
|
||||
|
||||
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
|
||||
results.push({ id: mergeId, success: true });
|
||||
|
||||
+245
-115
@@ -57,11 +57,16 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
SyncRequestType.AssetExifsV1,
|
||||
SyncRequestType.PartnerAssetsV1,
|
||||
SyncRequestType.PartnerAssetExifsV1,
|
||||
SyncRequestType.AlbumAssetsV1,
|
||||
SyncRequestType.AlbumsV1,
|
||||
SyncRequestType.AlbumUsersV1,
|
||||
SyncRequestType.AlbumToAssetsV1,
|
||||
SyncRequestType.AssetExifsV1,
|
||||
SyncRequestType.AlbumAssetExifsV1,
|
||||
SyncRequestType.PartnerAssetExifsV1,
|
||||
SyncRequestType.MemoriesV1,
|
||||
SyncRequestType.MemoryToAssetsV1,
|
||||
];
|
||||
|
||||
const throwSessionRequired = () => {
|
||||
@@ -76,7 +81,7 @@ export class SyncService extends BaseService {
|
||||
return throwSessionRequired();
|
||||
}
|
||||
|
||||
return this.syncRepository.getCheckpoints(sessionId);
|
||||
return this.syncCheckpointRepository.getAll(sessionId);
|
||||
}
|
||||
|
||||
async setAcks(auth: AuthDto, dto: SyncAckSetDto) {
|
||||
@@ -93,14 +98,11 @@ export class SyncService extends BaseService {
|
||||
throw new BadRequestException(`Invalid ack type: ${type}`);
|
||||
}
|
||||
|
||||
if (checkpoints[type]) {
|
||||
throw new BadRequestException('Only one ack per type is allowed');
|
||||
}
|
||||
|
||||
// TODO pick the latest ack for each type, instead of using the last one
|
||||
checkpoints[type] = { sessionId, type, ack };
|
||||
}
|
||||
|
||||
await this.syncRepository.upsertCheckpoints(Object.values(checkpoints));
|
||||
await this.syncCheckpointRepository.upsertAll(Object.values(checkpoints));
|
||||
}
|
||||
|
||||
async deleteAcks(auth: AuthDto, dto: SyncAckDeleteDto) {
|
||||
@@ -109,7 +111,7 @@ export class SyncService extends BaseService {
|
||||
return throwSessionRequired();
|
||||
}
|
||||
|
||||
await this.syncRepository.deleteCheckpoints(sessionId, dto.types);
|
||||
await this.syncCheckpointRepository.deleteAll(sessionId, dto.types);
|
||||
}
|
||||
|
||||
async stream(auth: AuthDto, response: Writable, dto: SyncStreamDto) {
|
||||
@@ -118,95 +120,72 @@ export class SyncService extends BaseService {
|
||||
return throwSessionRequired();
|
||||
}
|
||||
|
||||
const checkpoints = await this.syncRepository.getCheckpoints(sessionId);
|
||||
const checkpoints = await this.syncCheckpointRepository.getAll(sessionId);
|
||||
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
|
||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth),
|
||||
};
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
switch (type) {
|
||||
case SyncRequestType.UsersV1: {
|
||||
await this.syncUsersV1(response, checkpointMap);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.PartnersV1: {
|
||||
await this.syncPartnersV1(response, checkpointMap, auth);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.AssetsV1: {
|
||||
await this.syncAssetsV1(response, checkpointMap, auth);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.AssetExifsV1: {
|
||||
await this.syncAssetExifsV1(response, checkpointMap, auth);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.PartnerAssetsV1: {
|
||||
await this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.PartnerAssetExifsV1: {
|
||||
await this.syncPartnerAssetExifsV1(response, checkpointMap, auth, sessionId);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.AlbumsV1: {
|
||||
await this.syncAlbumsV1(response, checkpointMap, auth);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.AlbumUsersV1: {
|
||||
await this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
this.logger.warn(`Unsupported sync type: ${type}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const handler = handlers[type];
|
||||
await handler();
|
||||
}
|
||||
|
||||
response.end();
|
||||
}
|
||||
|
||||
private async syncUsersV1(response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deletes = this.syncRepository.getUserDeletes(checkpointMap[SyncEntityType.UserDeleteV1]);
|
||||
const deleteType = SyncEntityType.UserDeleteV1;
|
||||
const deletes = this.syncRepository.user.getDeletes(checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: SyncEntityType.UserDeleteV1, ids: [id], data });
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getUserUpserts(checkpointMap[SyncEntityType.UserV1]);
|
||||
const upsertType = SyncEntityType.UserV1;
|
||||
const upserts = this.syncRepository.user.getUpserts(checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.UserV1, ids: [updateId], data });
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPartnersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deletes = this.syncRepository.getPartnerDeletes(auth.user.id, checkpointMap[SyncEntityType.PartnerDeleteV1]);
|
||||
const deleteType = SyncEntityType.PartnerDeleteV1;
|
||||
const deletes = this.syncRepository.partner.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: SyncEntityType.PartnerDeleteV1, ids: [id], data });
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getPartnerUpserts(auth.user.id, checkpointMap[SyncEntityType.PartnerV1]);
|
||||
const upsertType = SyncEntityType.PartnerV1;
|
||||
const upserts = this.syncRepository.partner.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.PartnerV1, ids: [updateId], data });
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deletes = this.syncRepository.getAssetDeletes(auth.user.id, checkpointMap[SyncEntityType.AssetDeleteV1]);
|
||||
const deleteType = SyncEntityType.AssetDeleteV1;
|
||||
const deletes = this.syncRepository.asset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: SyncEntityType.AssetDeleteV1, ids: [id], data });
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getAssetUpserts(auth.user.id, checkpointMap[SyncEntityType.AssetV1]);
|
||||
const upsertType = SyncEntityType.AssetV1;
|
||||
const upserts = this.syncRepository.asset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.AssetV1, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,21 +195,17 @@ export class SyncService extends BaseService {
|
||||
auth: AuthDto,
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
||||
const upsertType = SyncEntityType.PartnerAssetV1;
|
||||
const deleteType = SyncEntityType.PartnerAssetDeleteV1;
|
||||
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
|
||||
const deletes = this.syncRepository.getPartnerAssetDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
|
||||
const deletes = this.syncRepository.partnerAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const partners = await this.syncRepository.getPartnerBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const partners = await this.syncRepository.partner.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.PartnerAssetV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
@@ -241,7 +216,7 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.getPartnerAssetsBackfill(partner.sharedById, startId, endId);
|
||||
const backfill = this.syncRepository.partnerAsset.getBackfill(partner.sharedById, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, {
|
||||
@@ -261,16 +236,17 @@ export class SyncService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getPartnerAssetsUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
const upserts = this.syncRepository.partnerAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetExifsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const upserts = this.syncRepository.getAssetExifsUpserts(auth.user.id, checkpointMap[SyncEntityType.AssetExifV1]);
|
||||
const upsertType = SyncEntityType.AssetExifV1;
|
||||
const upserts = this.syncRepository.assetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.AssetExifV1, ids: [updateId], data });
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,13 +257,11 @@ export class SyncService extends BaseService {
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.PartnerAssetExifBackfillV1;
|
||||
const upsertType = SyncEntityType.PartnerAssetExifV1;
|
||||
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const partners = await this.syncRepository.partner.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
|
||||
const upsertType = SyncEntityType.PartnerAssetExifV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
|
||||
const partners = await this.syncRepository.getPartnerBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
@@ -298,7 +272,7 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.getPartnerAssetExifsBackfill(partner.sharedById, startId, endId);
|
||||
const backfill = this.syncRepository.partnerAssetExif.getBackfill(partner.sharedById, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [partner.createId, updateId], data });
|
||||
@@ -314,40 +288,38 @@ export class SyncService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getPartnerAssetExifsUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
const upserts = this.syncRepository.partnerAssetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deletes = this.syncRepository.getAlbumDeletes(auth.user.id, checkpointMap[SyncEntityType.AlbumDeleteV1]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: SyncEntityType.AlbumDeleteV1, ids: [id], data });
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getAlbumUpserts(auth.user.id, checkpointMap[SyncEntityType.AlbumV1]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.AlbumV1, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
||||
const upsertType = SyncEntityType.AlbumUserV1;
|
||||
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
||||
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
|
||||
const deletes = this.syncRepository.getAlbumUserDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
|
||||
const deleteType = SyncEntityType.AlbumDeleteV1;
|
||||
const deletes = this.syncRepository.album.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.AlbumV1;
|
||||
const upserts = this.syncRepository.album.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
||||
const deletes = this.syncRepository.albumUser.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.AlbumUserV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
@@ -358,7 +330,7 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.getAlbumUsersBackfill(album.id, startId, endId);
|
||||
const backfill = this.syncRepository.albumUser.getBackfill(album.id, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||
@@ -374,7 +346,165 @@ export class SyncService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getAlbumUserUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
const upserts = this.syncRepository.albumUser.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.AlbumAssetV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
for (const album of albums) {
|
||||
const createId = album.createId;
|
||||
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.albumAsset.getBackfill(album.id, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
}
|
||||
} else if (albums.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: albums.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.albumAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetExifsV1(
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
auth: AuthDto,
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.AlbumAssetExifBackfillV1;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.AlbumAssetExifV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
for (const album of albums) {
|
||||
const createId = album.createId;
|
||||
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.albumAssetExif.getBackfill(album.id, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
}
|
||||
} else if (albums.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: albums.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.albumAssetExif.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumToAssetsV1(
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
auth: AuthDto,
|
||||
sessionId: string,
|
||||
) {
|
||||
const deleteType = SyncEntityType.AlbumToAssetDeleteV1;
|
||||
const deletes = this.syncRepository.albumToAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const backfillType = SyncEntityType.AlbumToAssetBackfillV1;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter(auth.user.id, backfillCheckpoint?.updateId);
|
||||
const upsertType = SyncEntityType.AlbumToAssetV1;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
for (const album of albums) {
|
||||
const createId = album.createId;
|
||||
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.albumToAsset.getBackfill(album.id, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
}
|
||||
} else if (albums.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: albums.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.albumToAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncMemoriesV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deleteType = SyncEntityType.MemoryDeleteV1;
|
||||
const deletes = this.syncRepository.memory.getDeletes(auth.user.id, checkpointMap[SyncEntityType.MemoryDeleteV1]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.MemoryV1;
|
||||
const upserts = this.syncRepository.memory.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncMemoryAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deleteType = SyncEntityType.MemoryToAssetDeleteV1;
|
||||
const deletes = this.syncRepository.memoryToAsset.getDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.MemoryToAssetV1;
|
||||
const upserts = this.syncRepository.memoryToAsset.getUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
@@ -382,7 +512,7 @@ export class SyncService extends BaseService {
|
||||
|
||||
private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) {
|
||||
const { type, sessionId, createId } = item;
|
||||
await this.syncRepository.upsertCheckpoints([
|
||||
await this.syncCheckpointRepository.upsertAll([
|
||||
{
|
||||
type,
|
||||
sessionId,
|
||||
|
||||
@@ -256,22 +256,17 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.memory.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_UPDATE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_CREATE: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ:
|
||||
case Permission.PERSON_UPDATE:
|
||||
case Permission.PERSON_DELETE:
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await access.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_REASSIGN: {
|
||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
+288
-325
@@ -1,12 +1,13 @@
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { Writable } from 'node:stream';
|
||||
import { AssetFace } from 'src/database';
|
||||
import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
|
||||
import { Albums, AssetJobStatus, Assets, DB, Exif, FaceSearch, Memories, Person, Sessions } from 'src/db';
|
||||
import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { AlbumUserRole, AssetType, AssetVisibility, MemoryType, SourceType, SyncRequestType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
@@ -16,6 +17,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
@@ -24,297 +26,291 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||
import { automock, ServiceOverrides, wait } from 'test/utils';
|
||||
import { automock, wait } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
|
||||
interface ClassConstructor<T = any> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
|
||||
type RepositoriesTypes = {
|
||||
activity: ActivityRepository;
|
||||
album: AlbumRepository;
|
||||
albumUser: AlbumUserRepository;
|
||||
asset: AssetRepository;
|
||||
assetJob: AssetJobRepository;
|
||||
config: ConfigRepository;
|
||||
crypto: CryptoRepository;
|
||||
database: DatabaseRepository;
|
||||
email: EmailRepository;
|
||||
job: JobRepository;
|
||||
user: UserRepository;
|
||||
logger: LoggingRepository;
|
||||
memory: MemoryRepository;
|
||||
notification: NotificationRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
search: SearchRepository;
|
||||
session: SessionRepository;
|
||||
sync: SyncRepository;
|
||||
systemMetadata: SystemMetadataRepository;
|
||||
versionHistory: VersionHistoryRepository;
|
||||
};
|
||||
type RepositoryMocks = { [K in keyof RepositoriesTypes]: Mocked<RepositoryInterface<RepositoriesTypes[K]>> };
|
||||
type RepositoryOptions = Partial<{ [K in keyof RepositoriesTypes]: 'mock' | 'real' }>;
|
||||
|
||||
type ContextRepositoryMocks<R extends RepositoryOptions> = {
|
||||
[K in keyof RepositoriesTypes as R[K] extends 'mock' ? K : never]: Mocked<RepositoryInterface<RepositoriesTypes[K]>>;
|
||||
type MediumTestOptions = {
|
||||
mock: ClassConstructor<any>[];
|
||||
real: ClassConstructor<any>[];
|
||||
database: Kysely<DB>;
|
||||
};
|
||||
|
||||
type ContextRepositories<R extends RepositoryOptions> = {
|
||||
[K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K];
|
||||
export const newMediumService = <S extends BaseService>(Service: ClassConstructor<S>, options: MediumTestOptions) => {
|
||||
const ctx = new MediumTestContext(Service, options);
|
||||
return { sut: ctx.sut, ctx };
|
||||
};
|
||||
|
||||
export type Context<R extends RepositoryOptions, S extends BaseService> = {
|
||||
export class MediumTestContext<S extends BaseService = BaseService> {
|
||||
private repoCache: Record<string, any> = {};
|
||||
private sutDeps: any[];
|
||||
|
||||
sut: S;
|
||||
mocks: ContextRepositoryMocks<R>;
|
||||
repos: ContextRepositories<R>;
|
||||
getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
|
||||
};
|
||||
database: Kysely<DB>;
|
||||
|
||||
export type SyncTestOptions = {
|
||||
db: Kysely<DB>;
|
||||
};
|
||||
constructor(
|
||||
Service: ClassConstructor<S>,
|
||||
private options: MediumTestOptions,
|
||||
) {
|
||||
this.sutDeps = this.makeDeps(options);
|
||||
this.sut = new Service(...this.sutDeps);
|
||||
this.database = options.database;
|
||||
}
|
||||
|
||||
export const newSyncAuthUser = () => {
|
||||
const user = mediumFactory.userInsert();
|
||||
const session = mediumFactory.sessionInsert({ userId: user.id });
|
||||
private makeDeps(options: MediumTestOptions) {
|
||||
const deps = BASE_SERVICE_DEPENDENCIES;
|
||||
|
||||
const auth = factory.auth({
|
||||
session,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
for (const dep of options.mock) {
|
||||
if (!deps.includes(dep)) {
|
||||
throw new Error(`Mocked repository ${dep.name} is not a valid dependency`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
auth,
|
||||
session,
|
||||
user,
|
||||
create: async (db: Kysely<DB>) => {
|
||||
await new UserRepository(db).create(user);
|
||||
await new SessionRepository(db).create(session);
|
||||
},
|
||||
};
|
||||
};
|
||||
for (const dep of options.real) {
|
||||
if (!deps.includes(dep)) {
|
||||
throw new Error(`Real repository ${dep.name} is not a valid dependency`);
|
||||
}
|
||||
}
|
||||
return (deps as ClassConstructor<any>[]).map((dep) => {
|
||||
if (options.real.includes(dep)) {
|
||||
return this.get(dep);
|
||||
}
|
||||
|
||||
export const newSyncTest = (options: SyncTestOptions) => {
|
||||
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
||||
database: options.db,
|
||||
repos: {
|
||||
sync: 'real',
|
||||
session: 'real',
|
||||
},
|
||||
});
|
||||
if (options.mock.includes(dep)) {
|
||||
return newMockRepository(dep);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
||||
get<T>(key: ClassConstructor<T>): T {
|
||||
if (!this.repoCache[key.name]) {
|
||||
const real = newRealRepository(key, this.options.database);
|
||||
this.repoCache[key.name] = real;
|
||||
}
|
||||
|
||||
return this.repoCache[key.name];
|
||||
}
|
||||
|
||||
getMock<T, R = Mocked<T>>(key: ClassConstructor<T>): R {
|
||||
const index = BASE_SERVICE_DEPENDENCIES.indexOf(key as any);
|
||||
if (index === -1 || !this.options.mock.includes(key)) {
|
||||
throw new Error(`getMock called with a key that is not a mock: ${key.name}`);
|
||||
}
|
||||
|
||||
return this.sutDeps[index] as R;
|
||||
}
|
||||
|
||||
async newUser(dto: Partial<Insertable<UserTable>> = {}) {
|
||||
const user = mediumFactory.userInsert(dto);
|
||||
const result = await this.get(UserRepository).create(user);
|
||||
return { user, result };
|
||||
}
|
||||
|
||||
async newPartner(dto: { sharedById: string; sharedWithId: string; inTimeline?: boolean }) {
|
||||
const partner = { inTimeline: true, ...dto };
|
||||
const result = await this.get(PartnerRepository).create(partner);
|
||||
return { partner, result };
|
||||
}
|
||||
|
||||
async newAsset(dto: Partial<Insertable<Assets>> = {}) {
|
||||
const asset = mediumFactory.assetInsert(dto);
|
||||
const result = await this.get(AssetRepository).create(asset);
|
||||
return { asset, result };
|
||||
}
|
||||
|
||||
async newMemory(dto: Partial<Insertable<Memories>> = {}) {
|
||||
const memory = mediumFactory.memoryInsert(dto);
|
||||
const result = await this.get(MemoryRepository).create(memory, new Set<string>());
|
||||
return { memory, result };
|
||||
}
|
||||
|
||||
async newMemoryAsset(dto: { memoryId: string; assetId: string }) {
|
||||
const result = await this.get(MemoryRepository).addAssetIds(dto.memoryId, [dto.assetId]);
|
||||
return { memoryAsset: dto, result };
|
||||
}
|
||||
|
||||
async newExif(dto: Insertable<Exif>) {
|
||||
const result = await this.get(AssetRepository).upsertExif(dto);
|
||||
return { result };
|
||||
}
|
||||
|
||||
async newAlbum(dto: Insertable<Albums>) {
|
||||
const album = mediumFactory.albumInsert(dto);
|
||||
const result = await this.get(AlbumRepository).create(album, [], []);
|
||||
return { album, result };
|
||||
}
|
||||
|
||||
async newAlbumAsset(albumAsset: { albumId: string; assetId: string }) {
|
||||
const result = await this.get(AlbumRepository).addAssetIds(albumAsset.albumId, [albumAsset.assetId]);
|
||||
return { albumAsset, result };
|
||||
}
|
||||
|
||||
async newAlbumUser(dto: { albumId: string; userId: string; role?: AlbumUserRole }) {
|
||||
const { albumId, userId, role = AlbumUserRole.EDITOR } = dto;
|
||||
const result = await this.get(AlbumUserRepository).create({ albumsId: albumId, usersId: userId, role });
|
||||
return { albumUser: { albumId, userId, role }, result };
|
||||
}
|
||||
|
||||
async newJobStatus(dto: Partial<Insertable<AssetJobStatus>> & { assetId: string }) {
|
||||
const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId });
|
||||
const result = await this.get(AssetRepository).upsertJobStatus(jobStatus);
|
||||
return { jobStatus, result };
|
||||
}
|
||||
|
||||
async newPerson(dto: Partial<Insertable<Person>> & { ownerId: string }) {
|
||||
const person = mediumFactory.personInsert(dto);
|
||||
const result = await this.get(PersonRepository).create(person);
|
||||
return { person, result };
|
||||
}
|
||||
|
||||
async newSession(dto: Partial<Insertable<Sessions>> & { userId: string }) {
|
||||
const session = mediumFactory.sessionInsert(dto);
|
||||
const result = await this.get(SessionRepository).create(session);
|
||||
return { session, result };
|
||||
}
|
||||
|
||||
async newSyncAuthUser() {
|
||||
const { user } = await this.newUser();
|
||||
const { session } = await this.newSession({ userId: user.id });
|
||||
const auth = factory.auth({
|
||||
session,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
auth,
|
||||
session,
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncTestContext extends MediumTestContext<SyncService> {
|
||||
constructor(database: Kysely<DB>) {
|
||||
super(SyncService, {
|
||||
database,
|
||||
real: [SyncRepository, SyncCheckpointRepository, SessionRepository],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
}
|
||||
|
||||
async syncStream(auth: AuthDto, types: SyncRequestType[]) {
|
||||
const stream = mediumFactory.syncStream();
|
||||
// Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
|
||||
await wait(2);
|
||||
await sut.stream(auth, stream, { types });
|
||||
await this.sut.stream(auth, stream, { types });
|
||||
|
||||
return stream.getResponse();
|
||||
};
|
||||
|
||||
return {
|
||||
sut,
|
||||
mocks,
|
||||
repos,
|
||||
getRepository,
|
||||
testSync,
|
||||
};
|
||||
};
|
||||
|
||||
export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
|
||||
Service: ClassConstructor<S>,
|
||||
options: {
|
||||
database: Kysely<DB>;
|
||||
repos: R;
|
||||
},
|
||||
): Context<R, S> => {
|
||||
const repos: Partial<RepositoriesTypes> = {};
|
||||
const mocks: Partial<RepositoryMocks> = {};
|
||||
|
||||
const loggerMock = getRepositoryMock('logger') as Mocked<LoggingRepository>;
|
||||
loggerMock.setContext.mockImplementation(() => {});
|
||||
repos.logger = loggerMock;
|
||||
|
||||
for (const [_key, type] of Object.entries(options.repos)) {
|
||||
if (type === 'real') {
|
||||
const key = _key as keyof RepositoriesTypes;
|
||||
repos[key] = getRepository(key, options.database) as any;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 'mock') {
|
||||
const key = _key as keyof RepositoryMocks;
|
||||
mocks[key] = getRepositoryMock(key) as any;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const makeRepository = <K extends keyof RepositoriesTypes>(key: K) => {
|
||||
return repos[key] || getRepository(key, options.database);
|
||||
};
|
||||
async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
|
||||
const acks: Record<string, string> = {};
|
||||
for (const { type, ack } of response) {
|
||||
acks[type] = ack;
|
||||
}
|
||||
|
||||
const deps = asDeps({ ...mocks, ...repos } as ServiceOverrides);
|
||||
const sut = new Service(...deps);
|
||||
await this.sut.setAcks(auth, { acks: Object.values(acks) });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sut,
|
||||
mocks,
|
||||
repos,
|
||||
getRepository: makeRepository,
|
||||
} as Context<R, S>;
|
||||
};
|
||||
|
||||
export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
|
||||
const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||
switch (key) {
|
||||
case 'activity': {
|
||||
return new ActivityRepository(db);
|
||||
case AccessRepository:
|
||||
case AlbumRepository:
|
||||
case AlbumUserRepository:
|
||||
case ActivityRepository:
|
||||
case AssetRepository:
|
||||
case AssetJobRepository:
|
||||
case MemoryRepository:
|
||||
case NotificationRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case SearchRepository:
|
||||
case SessionRepository:
|
||||
case SyncRepository:
|
||||
case SyncCheckpointRepository:
|
||||
case SystemMetadataRepository:
|
||||
case UserRepository:
|
||||
case VersionHistoryRepository: {
|
||||
return new key(db);
|
||||
}
|
||||
|
||||
case 'album': {
|
||||
return new AlbumRepository(db);
|
||||
case ConfigRepository:
|
||||
case CryptoRepository: {
|
||||
return new key();
|
||||
}
|
||||
|
||||
case 'albumUser': {
|
||||
return new AlbumUserRepository(db);
|
||||
case DatabaseRepository: {
|
||||
return new key(db, LoggingRepository.create(), new ConfigRepository());
|
||||
}
|
||||
|
||||
case 'asset': {
|
||||
return new AssetRepository(db);
|
||||
case EmailRepository: {
|
||||
return new key(LoggingRepository.create());
|
||||
}
|
||||
|
||||
case 'assetJob': {
|
||||
return new AssetJobRepository(db);
|
||||
}
|
||||
|
||||
case 'config': {
|
||||
return new ConfigRepository();
|
||||
}
|
||||
|
||||
case 'crypto': {
|
||||
return new CryptoRepository();
|
||||
}
|
||||
|
||||
case 'database': {
|
||||
return new DatabaseRepository(db, LoggingRepository.create(), new ConfigRepository());
|
||||
}
|
||||
|
||||
case 'email': {
|
||||
return new EmailRepository(LoggingRepository.create());
|
||||
}
|
||||
|
||||
case 'logger': {
|
||||
return LoggingRepository.create();
|
||||
}
|
||||
|
||||
case 'memory': {
|
||||
return new MemoryRepository(db);
|
||||
}
|
||||
|
||||
case 'notification': {
|
||||
return new NotificationRepository(db);
|
||||
}
|
||||
|
||||
case 'partner': {
|
||||
return new PartnerRepository(db);
|
||||
}
|
||||
|
||||
case 'person': {
|
||||
return new PersonRepository(db);
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
return new SearchRepository(db);
|
||||
}
|
||||
|
||||
case 'session': {
|
||||
return new SessionRepository(db);
|
||||
}
|
||||
|
||||
case 'sync': {
|
||||
return new SyncRepository(db);
|
||||
}
|
||||
|
||||
case 'systemMetadata': {
|
||||
return new SystemMetadataRepository(db);
|
||||
}
|
||||
|
||||
case 'user': {
|
||||
return new UserRepository(db);
|
||||
}
|
||||
|
||||
case 'versionHistory': {
|
||||
return new VersionHistoryRepository(db);
|
||||
case LoggingRepository as unknown as ClassConstructor<LoggingRepository>: {
|
||||
return new key() as unknown as T;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Invalid repository key: ${key}`);
|
||||
throw new Error(`Unable to create repository instance for key: ${key?.name || key}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
switch (key) {
|
||||
case 'activity': {
|
||||
return automock(ActivityRepository) as Mocked<RepositoryInterface<ActivityRepository>>;
|
||||
case ActivityRepository:
|
||||
case AlbumRepository:
|
||||
case AssetRepository:
|
||||
case AssetJobRepository:
|
||||
case ConfigRepository:
|
||||
case CryptoRepository:
|
||||
case MemoryRepository:
|
||||
case NotificationRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case SessionRepository:
|
||||
case SyncRepository:
|
||||
case SyncCheckpointRepository:
|
||||
case SystemMetadataRepository:
|
||||
case UserRepository:
|
||||
case VersionHistoryRepository: {
|
||||
return automock(key);
|
||||
}
|
||||
|
||||
case 'album': {
|
||||
return automock(AlbumRepository);
|
||||
}
|
||||
|
||||
case 'asset': {
|
||||
return automock(AssetRepository);
|
||||
}
|
||||
|
||||
case 'assetJob': {
|
||||
return automock(AssetJobRepository);
|
||||
}
|
||||
|
||||
case 'config': {
|
||||
return automock(ConfigRepository);
|
||||
}
|
||||
|
||||
case 'crypto': {
|
||||
return automock(CryptoRepository);
|
||||
}
|
||||
|
||||
case 'database': {
|
||||
case DatabaseRepository: {
|
||||
return automock(DatabaseRepository, {
|
||||
args: [
|
||||
undefined,
|
||||
{
|
||||
setContext: () => {},
|
||||
},
|
||||
{ getEnv: () => ({ database: { vectorExtension: '' } }) },
|
||||
],
|
||||
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
|
||||
});
|
||||
}
|
||||
|
||||
case 'email': {
|
||||
return automock(EmailRepository, {
|
||||
args: [
|
||||
{
|
||||
setContext: () => {},
|
||||
},
|
||||
],
|
||||
});
|
||||
case EmailRepository: {
|
||||
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
|
||||
}
|
||||
|
||||
case 'job': {
|
||||
case EventRepository: {
|
||||
return automock(EventRepository, { args: [undefined, undefined, { setContext: () => {} }] });
|
||||
}
|
||||
|
||||
case JobRepository: {
|
||||
return automock(JobRepository, {
|
||||
args: [
|
||||
undefined,
|
||||
@@ -327,45 +323,13 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
});
|
||||
}
|
||||
|
||||
case 'logger': {
|
||||
case LoggingRepository as unknown as ClassConstructor<T>: {
|
||||
const configMock = { getEnv: () => ({ noColor: false }) };
|
||||
return automock(LoggingRepository, { args: [undefined, configMock], strict: false });
|
||||
}
|
||||
|
||||
case 'memory': {
|
||||
return automock(MemoryRepository);
|
||||
}
|
||||
|
||||
case 'notification': {
|
||||
return automock(NotificationRepository);
|
||||
}
|
||||
|
||||
case 'partner': {
|
||||
return automock(PartnerRepository);
|
||||
}
|
||||
|
||||
case 'person': {
|
||||
return automock(PersonRepository);
|
||||
}
|
||||
|
||||
case 'session': {
|
||||
return automock(SessionRepository);
|
||||
}
|
||||
|
||||
case 'sync': {
|
||||
return automock(SyncRepository);
|
||||
}
|
||||
|
||||
case 'systemMetadata': {
|
||||
return automock(SystemMetadataRepository);
|
||||
}
|
||||
|
||||
case 'user': {
|
||||
return automock(UserRepository);
|
||||
}
|
||||
|
||||
case 'versionHistory': {
|
||||
return automock(VersionHistoryRepository);
|
||||
case StorageRepository: {
|
||||
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -374,55 +338,6 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const asDeps = (repositories: ServiceOverrides) => {
|
||||
return [
|
||||
repositories.logger || getRepositoryMock('logger'), // logger
|
||||
repositories.access, // access
|
||||
repositories.activity || getRepositoryMock('activity'),
|
||||
repositories.album || getRepositoryMock('album'),
|
||||
repositories.albumUser,
|
||||
repositories.apiKey,
|
||||
repositories.asset || getRepositoryMock('asset'),
|
||||
repositories.assetJob || getRepositoryMock('assetJob'),
|
||||
repositories.audit,
|
||||
repositories.config || getRepositoryMock('config'),
|
||||
repositories.cron,
|
||||
repositories.crypto || getRepositoryMock('crypto'),
|
||||
repositories.database || getRepositoryMock('database'),
|
||||
repositories.downloadRepository,
|
||||
repositories.duplicateRepository,
|
||||
repositories.email || getRepositoryMock('email'),
|
||||
repositories.event,
|
||||
repositories.job || getRepositoryMock('job'),
|
||||
repositories.library,
|
||||
repositories.machineLearning,
|
||||
repositories.map,
|
||||
repositories.media,
|
||||
repositories.memory || getRepositoryMock('memory'),
|
||||
repositories.metadata,
|
||||
repositories.move,
|
||||
repositories.notification || getRepositoryMock('notification'),
|
||||
repositories.oauth,
|
||||
repositories.partner || getRepositoryMock('partner'),
|
||||
repositories.person || getRepositoryMock('person'),
|
||||
repositories.process,
|
||||
repositories.search,
|
||||
repositories.serverInfo,
|
||||
repositories.session || getRepositoryMock('session'),
|
||||
repositories.sharedLink,
|
||||
repositories.stack,
|
||||
repositories.storage,
|
||||
repositories.sync || getRepositoryMock('sync'),
|
||||
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
|
||||
repositories.tag,
|
||||
repositories.telemetry,
|
||||
repositories.trash,
|
||||
repositories.user,
|
||||
repositories.versionHistory || getRepositoryMock('versionHistory'),
|
||||
repositories.view,
|
||||
];
|
||||
};
|
||||
|
||||
const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
|
||||
const id = asset.id || newUuid();
|
||||
const now = newDate();
|
||||
@@ -532,6 +447,8 @@ const personInsert = (person: Partial<Insertable<Person>> & { ownerId: string })
|
||||
};
|
||||
};
|
||||
|
||||
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
|
||||
|
||||
const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertable<Sessions>> & { userId: string }) => {
|
||||
const defaults: Insertable<Sessions> = {
|
||||
id,
|
||||
@@ -549,15 +466,40 @@ const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertabl
|
||||
const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
|
||||
const id = user.id || newUuid();
|
||||
|
||||
const defaults: Insertable<UserTable> = {
|
||||
const defaults = {
|
||||
email: `${id}@immich.cloud`,
|
||||
name: `User ${id}`,
|
||||
deletedAt: null,
|
||||
isAdmin: false,
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: true,
|
||||
};
|
||||
|
||||
return { ...defaults, ...user, id };
|
||||
};
|
||||
|
||||
const memoryInsert = (memory: Partial<Insertable<Memories>> = {}) => {
|
||||
const id = memory.id || newUuid();
|
||||
const date = newDate();
|
||||
|
||||
const defaults: Insertable<Memories> = {
|
||||
id,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
deletedAt: null,
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2025 },
|
||||
showAt: null,
|
||||
hideAt: null,
|
||||
seenAt: null,
|
||||
isSaved: false,
|
||||
memoryAt: date,
|
||||
ownerId: memory.ownerId || newUuid(),
|
||||
};
|
||||
|
||||
return { ...defaults, ...memory, id };
|
||||
};
|
||||
|
||||
class CustomWritable extends Writable {
|
||||
private data = '';
|
||||
|
||||
@@ -579,6 +521,24 @@ const syncStream = () => {
|
||||
return new CustomWritable();
|
||||
};
|
||||
|
||||
const loginDetails = () => {
|
||||
return { isSecure: false, clientIp: '', deviceType: '', deviceOS: '' };
|
||||
};
|
||||
|
||||
const loginResponse = (): LoginResponseDto => {
|
||||
const user = userInsert({});
|
||||
return {
|
||||
accessToken: 'access-token',
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
name: user.name,
|
||||
profileImagePath: user.profileImagePath,
|
||||
isAdmin: user.isAdmin,
|
||||
shouldChangePassword: user.shouldChangePassword,
|
||||
isOnboarded: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const mediumFactory = {
|
||||
assetInsert,
|
||||
assetFaceInsert,
|
||||
@@ -589,4 +549,7 @@ export const mediumFactory = {
|
||||
sessionInsert,
|
||||
syncStream,
|
||||
userInsert,
|
||||
memoryInsert,
|
||||
loginDetails,
|
||||
loginResponse,
|
||||
};
|
||||
|
||||
@@ -1,44 +1,33 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
describe(AssetService.name, () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let assetRepo: AssetRepository;
|
||||
let userRepo: UserRepository;
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const createSut = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AssetService, {
|
||||
database: db || defaultDatabase,
|
||||
repos: {
|
||||
asset: 'real',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
|
||||
assetRepo = new AssetRepository(defaultDatabase);
|
||||
userRepo = new UserRepository(defaultDatabase);
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AssetService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [AssetRepository],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(AssetService.name, () => {
|
||||
describe('getStatistics', () => {
|
||||
it('should return stats as numbers, not strings', async () => {
|
||||
const { sut } = createSut();
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user.id });
|
||||
|
||||
await userRepo.create(user);
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, fileSizeInByte: 12_345 });
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 });
|
||||
});
|
||||
|
||||
@@ -1,74 +1,63 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { partners_delete_audit } from 'src/schema/functions';
|
||||
import { mediumFactory } from 'test/medium.factory';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MediumTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
describe('audit', () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let assetRepo: AssetRepository;
|
||||
let userRepo: UserRepository;
|
||||
let partnerRepo: PartnerRepository;
|
||||
let ctx: MediumTestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
|
||||
assetRepo = new AssetRepository(defaultDatabase);
|
||||
userRepo = new UserRepository(defaultDatabase);
|
||||
partnerRepo = new PartnerRepository(defaultDatabase);
|
||||
ctx = new MediumTestContext(BaseService, {
|
||||
database: await getKyselyDB(),
|
||||
real: [],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
});
|
||||
|
||||
describe(partners_delete_audit.name, () => {
|
||||
it('should not cascade user deletes to partners_audit', async () => {
|
||||
const user1 = mediumFactory.userInsert();
|
||||
const user2 = mediumFactory.userInsert();
|
||||
|
||||
await Promise.all([userRepo.create(user1), userRepo.create(user2)]);
|
||||
const partnerRepo = ctx.get(PartnerRepository);
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
await userRepo.delete(user1, true);
|
||||
|
||||
await expect(
|
||||
defaultDatabase.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(),
|
||||
ctx.database.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assets_audit', () => {
|
||||
it('should not cascade user deletes to assets_audit', async () => {
|
||||
const user = mediumFactory.userInsert();
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user.id });
|
||||
|
||||
await userRepo.create(user);
|
||||
await assetRepo.create(asset);
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await userRepo.delete(user, true);
|
||||
|
||||
await expect(
|
||||
defaultDatabase.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(),
|
||||
ctx.database.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exif', () => {
|
||||
it('should automatically set updatedAt and updateId when the row is updated', async () => {
|
||||
const user = mediumFactory.userInsert();
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user.id });
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
await userRepo.create(user);
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const before = await defaultDatabase
|
||||
const before = await ctx.database
|
||||
.selectFrom('exif')
|
||||
.select(['updatedAt', 'updateId'])
|
||||
.where('assetId', '=', asset.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon 2' });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon 2' });
|
||||
|
||||
const after = await defaultDatabase
|
||||
const after = await ctx.database
|
||||
.selectFrom('exif')
|
||||
.select(['updatedAt', 'updateId'])
|
||||
.where('assetId', '=', asset.id)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { hash } from 'bcrypt';
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AuthType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AuthService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [
|
||||
AccessRepository,
|
||||
ConfigRepository,
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
SessionRepository,
|
||||
SystemMetadataRepository,
|
||||
UserRepository,
|
||||
],
|
||||
mock: [LoggingRepository, StorageRepository, EventRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(AuthService.name, () => {
|
||||
describe('adminSignUp', () => {
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { sut } = setup();
|
||||
const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' };
|
||||
|
||||
await expect(sut.adminSignUp(dto)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
isAdmin: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow a second admin to sign up', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
await ctx.newUser({ isAdmin: true });
|
||||
const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' };
|
||||
|
||||
const response = sut.adminSignUp(dto);
|
||||
await expect(response).rejects.toThrow(BadRequestException);
|
||||
await expect(response).rejects.toThrow('The server already has an admin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should reject an incorrect password', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const password = 'password';
|
||||
const passwordHashed = await hash(password, 10);
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const dto = { email: user.email, password: 'wrong-password' };
|
||||
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails())).rejects.toThrow('Incorrect email or password');
|
||||
});
|
||||
|
||||
it('should accept a correct password and return a login response', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const password = 'password';
|
||||
const passwordHashed = await hash(password, 10);
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const dto = { email: user.email, password };
|
||||
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails())).resolves.toEqual({
|
||||
accessToken: expect.any(String),
|
||||
isAdmin: user.isAdmin,
|
||||
isOnboarded: false,
|
||||
name: user.name,
|
||||
profileImagePath: user.profileImagePath,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
shouldChangePassword: user.shouldChangePassword,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should logout', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
||||
successful: true,
|
||||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should cleanup the session', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const sessionRepo = ctx.get(SessionRepository);
|
||||
const eventRepo = ctx.getMock(EventRepository);
|
||||
const { user } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user.id });
|
||||
const auth = factory.auth({ session, user });
|
||||
eventRepo.emit.mockResolvedValue();
|
||||
|
||||
await expect(sessionRepo.get(session.id)).resolves.toEqual(expect.objectContaining({ id: session.id }));
|
||||
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
||||
successful: true,
|
||||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
await expect(sessionRepo.get(session.id)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should change the password and login with it', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const dto = { password: 'password', newPassword: 'new-password' };
|
||||
const passwordHashed = await hash(dto.password, 10);
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const response = await sut.changePassword(auth, dto);
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
}),
|
||||
);
|
||||
expect((response as any).password).not.toBeDefined();
|
||||
|
||||
await expect(
|
||||
sut.login({ email: user.email, password: dto.newPassword }, mediumFactory.loginDetails()),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should validate the current password', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const dto = { password: 'wrong-password', newPassword: 'new-password' };
|
||||
const passwordHashed = await hash('password', 10);
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const response = sut.changePassword(auth, dto);
|
||||
await expect(response).rejects.toThrow(BadRequestException);
|
||||
await expect(response).rejects.toThrow('Wrong password');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,72 +1,138 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DB } from 'src/db';
|
||||
import { AssetFileType } from 'src/enum';
|
||||
import { AssetFileType, MemoryType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(MemoryService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [
|
||||
AccessRepository,
|
||||
AssetRepository,
|
||||
DatabaseRepository,
|
||||
MemoryRepository,
|
||||
UserRepository,
|
||||
SystemMetadataRepository,
|
||||
UserRepository,
|
||||
PartnerRepository,
|
||||
],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
};
|
||||
|
||||
describe(MemoryService.name, () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const createSut = (db?: Kysely<DB>) => {
|
||||
return newMediumService(MemoryService, {
|
||||
database: db || defaultDatabase,
|
||||
repos: {
|
||||
asset: 'real',
|
||||
database: 'real',
|
||||
memory: 'real',
|
||||
user: 'real',
|
||||
systemMetadata: 'real',
|
||||
partner: 'real',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
const userRepo = new UserRepository(defaultDatabase);
|
||||
const admin = mediumFactory.userInsert({ isAdmin: true });
|
||||
await userRepo.create(admin);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new memory', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const dto = {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021),
|
||||
};
|
||||
|
||||
await expect(sut.create(auth, dto)).resolves.toEqual({
|
||||
id: expect.any(String),
|
||||
type: dto.type,
|
||||
data: dto.data,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
isSaved: false,
|
||||
memoryAt: dto.memoryAt,
|
||||
ownerId: user.id,
|
||||
assets: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new memory (with assets)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const auth = factory.auth({ user });
|
||||
const dto = {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021),
|
||||
assetIds: [asset1.id, asset2.id],
|
||||
};
|
||||
|
||||
await expect(sut.create(auth, dto)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
assets: [expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new memory and ignore assets the user does not have access to', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset: asset1 } = await ctx.newAsset({ ownerId: user1.id });
|
||||
const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const auth = factory.auth({ user: user1 });
|
||||
const dto = {
|
||||
type: MemoryType.ON_THIS_DAY,
|
||||
data: { year: 2021 },
|
||||
memoryAt: new Date(2021),
|
||||
assetIds: [asset1.id, asset2.id],
|
||||
};
|
||||
|
||||
await expect(sut.create(auth, dto)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
assets: [expect.objectContaining({ id: asset1.id })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onMemoryCreate', () => {
|
||||
it('should work on an empty database', async () => {
|
||||
const { sut } = createSut();
|
||||
const { sut } = setup();
|
||||
await expect(sut.onMemoriesCreate()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should create a memory from an asset', async () => {
|
||||
const { sut, repos, getRepository } = createSut();
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const now = DateTime.fromObject({ year: 2025, month: 2, day: 25 }, { zone: 'utc' }) as DateTime<true>;
|
||||
const user = mediumFactory.userInsert();
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: user.id,
|
||||
localDateTime: now.minus({ years: 1 }).toISO(),
|
||||
});
|
||||
const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: asset.id });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const assetRepo = getRepository('asset');
|
||||
|
||||
await userRepo.create(user);
|
||||
await assetRepo.create(asset);
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, localDateTime: now.minus({ years: 1 }).toISO() });
|
||||
await Promise.all([
|
||||
assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }),
|
||||
ctx.newExif({ assetId: asset.id, make: 'Canon' }),
|
||||
ctx.newJobStatus({ assetId: asset.id }),
|
||||
assetRepo.upsertFiles([
|
||||
{ assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' },
|
||||
{ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' },
|
||||
]),
|
||||
assetRepo.upsertJobStatus(jobStatus),
|
||||
]);
|
||||
|
||||
vi.setSystemTime(now.toJSDate());
|
||||
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await repos.memory.search(user.id, {});
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -88,16 +154,11 @@ describe(MemoryService.name, () => {
|
||||
});
|
||||
|
||||
it('should not generate a memory twice for the same day', async () => {
|
||||
const { sut, repos, getRepository } = createSut();
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const now = DateTime.fromObject({ year: 2025, month: 2, day: 20 }, { zone: 'utc' }) as DateTime<true>;
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const memoryRepo = getRepository('memory');
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
await repos.user.create(user);
|
||||
|
||||
const { user } = await ctx.newUser();
|
||||
for (const dto of [
|
||||
{
|
||||
ownerId: user.id,
|
||||
@@ -112,11 +173,10 @@ describe(MemoryService.name, () => {
|
||||
localDateTime: now.minus({ year: 1 }).plus({ days: 5 }).toISO(),
|
||||
},
|
||||
]) {
|
||||
const asset = mediumFactory.assetInsert(dto);
|
||||
await assetRepo.create(asset);
|
||||
const { asset } = await ctx.newAsset(dto);
|
||||
await Promise.all([
|
||||
assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }),
|
||||
assetRepo.upsertJobStatus(mediumFactory.assetJobStatusInsert({ assetId: asset.id })),
|
||||
ctx.newExif({ assetId: asset.id, make: 'Canon' }),
|
||||
ctx.newJobStatus({ assetId: asset.id }),
|
||||
assetRepo.upsertFiles([
|
||||
{ assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' },
|
||||
{ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' },
|
||||
@@ -125,13 +185,13 @@ describe(MemoryService.name, () => {
|
||||
}
|
||||
|
||||
vi.setSystemTime(now.toJSDate());
|
||||
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.length).toBe(1);
|
||||
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memoriesAfter = await memoryRepo.search(user.id, {});
|
||||
expect(memoriesAfter.length).toBe(1);
|
||||
});
|
||||
@@ -139,7 +199,7 @@ describe(MemoryService.name, () => {
|
||||
|
||||
describe('onMemoriesCleanup', () => {
|
||||
it('should run without error', async () => {
|
||||
const { sut } = createSut();
|
||||
const { sut } = setup();
|
||||
await expect(sut.onMemoriesCleanup()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PersonService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [AccessRepository, DatabaseRepository, PersonRepository],
|
||||
mock: [LoggingRepository, StorageRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(PersonService.name, () => {
|
||||
describe('delete', () => {
|
||||
it('should throw an error when there is no access', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
const personId = factory.uuid();
|
||||
await expect(sut.delete(auth, personId)).rejects.toThrow('Not found or no person.delete access');
|
||||
});
|
||||
|
||||
it('should delete the person', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const personRepo = ctx.get(PersonRepository);
|
||||
const storageMock = ctx.getMock(StorageRepository);
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const auth = factory.auth({ user });
|
||||
storageMock.unlink.mockResolvedValue();
|
||||
|
||||
await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id }));
|
||||
await expect(sut.delete(auth, person.id)).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person.id)).resolves.toBeUndefined();
|
||||
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
it('should throw an error when there is no access', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
const personId = factory.uuid();
|
||||
await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access');
|
||||
});
|
||||
|
||||
it('should delete the person', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const storageMock = ctx.getMock(StorageRepository);
|
||||
const personRepo = ctx.get(PersonRepository);
|
||||
const { user } = await ctx.newUser();
|
||||
const { person: person1 } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { person: person2 } = await ctx.newPerson({ ownerId: user.id });
|
||||
const auth = factory.auth({ user });
|
||||
storageMock.unlink.mockResolvedValue();
|
||||
|
||||
await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person1.id)).resolves.toBeUndefined();
|
||||
await expect(personRepo.getById(person2.id)).resolves.toBeUndefined();
|
||||
|
||||
expect(storageMock.unlink).toHaveBeenCalledTimes(2);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(person1.thumbnailPath);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,72 +2,65 @@ import { Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DB } from 'src/db';
|
||||
import { ImmichEnvironment, JobName, JobStatus } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
describe(UserService.name, () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const createSut = (db?: Kysely<DB>) => {
|
||||
process.env.IMMICH_ENV = ImmichEnvironment.TESTING;
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
process.env.IMMICH_ENV = ImmichEnvironment.TESTING;
|
||||
|
||||
return newMediumService(UserService, {
|
||||
database: db || defaultDatabase,
|
||||
repos: {
|
||||
user: 'real',
|
||||
crypto: 'real',
|
||||
config: 'real',
|
||||
job: 'mock',
|
||||
systemMetadata: 'real',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
const { repos } = createSut();
|
||||
await repos.user.create({ isAdmin: true, email: 'admin@immich.cloud' });
|
||||
return newMediumService(UserService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
||||
mock: [LoggingRepository, JobRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
const { ctx } = setup();
|
||||
await ctx.newUser({ isAdmin: true, email: 'admin@immich.cloud' });
|
||||
});
|
||||
|
||||
describe(UserService.name, () => {
|
||||
describe('create', () => {
|
||||
it('should create a user', async () => {
|
||||
const { sut } = createSut();
|
||||
const { sut } = setup();
|
||||
const user = mediumFactory.userInsert();
|
||||
|
||||
await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual(
|
||||
expect.objectContaining({ name: user.name, email: user.email }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject user with duplicate email', async () => {
|
||||
const { sut } = createSut();
|
||||
|
||||
const { sut } = setup();
|
||||
const user = mediumFactory.userInsert();
|
||||
|
||||
await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email });
|
||||
await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists');
|
||||
});
|
||||
|
||||
it('should not return password', async () => {
|
||||
const { sut } = createSut();
|
||||
const { sut } = setup();
|
||||
const dto = mediumFactory.userInsert({ password: 'password' });
|
||||
|
||||
const user = await sut.createUser({ email: dto.email, password: 'password' });
|
||||
|
||||
expect((user as any).password).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should get users', async () => {
|
||||
const { sut, repos } = createSut();
|
||||
const user1 = mediumFactory.userInsert();
|
||||
const user2 = mediumFactory.userInsert();
|
||||
|
||||
await Promise.all([repos.user.create(user1), repos.user.create(user2)]);
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth = factory.auth({ user: user1 });
|
||||
|
||||
await expect(sut.search(auth)).resolves.toEqual(
|
||||
@@ -81,10 +74,8 @@ describe(UserService.name, () => {
|
||||
|
||||
describe('get', () => {
|
||||
it('should get a user', async () => {
|
||||
const { sut, repos } = createSut();
|
||||
const user = mediumFactory.userInsert();
|
||||
|
||||
await repos.user.create(user);
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await expect(sut.get(user.id)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -96,11 +87,8 @@ describe(UserService.name, () => {
|
||||
});
|
||||
|
||||
it('should not return password', async () => {
|
||||
const { sut, repos } = createSut();
|
||||
const user = mediumFactory.userInsert();
|
||||
|
||||
await repos.user.create(user);
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const result = await sut.get(user.id);
|
||||
|
||||
expect((result as any).password).toBeUndefined();
|
||||
@@ -109,10 +97,9 @@ describe(UserService.name, () => {
|
||||
|
||||
describe('updateMe', () => {
|
||||
it('should update a user', async () => {
|
||||
const { sut, repos: repositories } = createSut();
|
||||
|
||||
const before = await repositories.user.create(mediumFactory.userInsert());
|
||||
const auth = factory.auth({ user: { id: before.id } });
|
||||
const { sut, ctx } = setup();
|
||||
const { user, result: before } = await ctx.newUser();
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
const after = await sut.updateMe(auth, { name: `${before.name} Updated` });
|
||||
|
||||
expect(before.updatedAt).toBeDefined();
|
||||
@@ -128,17 +115,13 @@ describe(UserService.name, () => {
|
||||
activationKey:
|
||||
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
|
||||
};
|
||||
const { sut, repos } = createSut();
|
||||
const user = mediumFactory.userInsert();
|
||||
await repos.user.create(user);
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
|
||||
await expect(sut.getLicense(auth)).rejects.toThrowError();
|
||||
const after = await sut.setLicense(auth, license);
|
||||
|
||||
expect(after.licenseKey).toEqual(license.licenseKey);
|
||||
expect(after.activationKey).toEqual(license.activationKey);
|
||||
|
||||
const getResponse = await sut.getLicense(auth);
|
||||
expect(getResponse).toEqual(after);
|
||||
});
|
||||
@@ -146,7 +129,7 @@ describe(UserService.name, () => {
|
||||
|
||||
describe.sequential('handleUserDeleteCheck', () => {
|
||||
beforeEach(async () => {
|
||||
const { sut } = createSut();
|
||||
const { sut } = setup();
|
||||
// These tests specifically have to be sequential otherwise we hit race conditions with config changes applying in incorrect tests
|
||||
const config = await sut.getConfig({ withCache: false });
|
||||
config.user.deleteDelay = 7;
|
||||
@@ -154,52 +137,43 @@ describe(UserService.name, () => {
|
||||
});
|
||||
|
||||
it('should work when there are no deleted users', async () => {
|
||||
const { sut, mocks } = createSut();
|
||||
mocks.job.queueAll.mockResolvedValue(void 0);
|
||||
|
||||
const { sut, ctx } = setup();
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
jobMock.queueAll.mockResolvedValue(void 0);
|
||||
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
});
|
||||
|
||||
it('should work when there is a user to delete', async () => {
|
||||
const { sut, repos, mocks } = createSut(await getKyselyDB());
|
||||
mocks.job.queueAll.mockResolvedValue(void 0);
|
||||
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() });
|
||||
await repos.user.create(user);
|
||||
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
const { user } = await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() });
|
||||
jobMock.queueAll.mockResolvedValue(void 0);
|
||||
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([
|
||||
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([
|
||||
{ name: JobName.USER_DELETION, data: { id: user.id } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip a recently deleted user', async () => {
|
||||
const { sut, repos, mocks } = createSut(await getKyselyDB());
|
||||
mocks.job.queueAll.mockResolvedValue(void 0);
|
||||
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() });
|
||||
await repos.user.create(user);
|
||||
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() });
|
||||
jobMock.queueAll.mockResolvedValue(void 0);
|
||||
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
});
|
||||
|
||||
it('should respect a custom user delete delay', async () => {
|
||||
const { sut, repos, mocks } = createSut(await getKyselyDB());
|
||||
mocks.job.queueAll.mockResolvedValue(void 0);
|
||||
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() });
|
||||
await repos.user.create(user);
|
||||
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() });
|
||||
jobMock.queueAll.mockResolvedValue(void 0);
|
||||
const config = await sut.getConfig({ withCache: false });
|
||||
config.user.deleteDelay = 30;
|
||||
|
||||
await sut.updateConfig(config);
|
||||
|
||||
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,38 +2,40 @@ import { Kysely } from 'kysely';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { DB } from 'src/db';
|
||||
import { JobName } from 'src/enum';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
describe(VersionService.name, () => {
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(VersionService, {
|
||||
database: db || defaultDatabase,
|
||||
repos: {
|
||||
job: 'mock',
|
||||
database: 'real',
|
||||
versionHistory: 'real',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(VersionService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [DatabaseRepository, VersionHistoryRepository],
|
||||
mock: [LoggingRepository, JobRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(VersionService.name, () => {
|
||||
describe('onBootstrap', () => {
|
||||
it('record the current version on startup', async () => {
|
||||
const { sut, repos } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
const versionHistoryRepo = ctx.get(VersionHistoryRepository);
|
||||
|
||||
const itemsBefore = await repos.versionHistory.getAll();
|
||||
const itemsBefore = await versionHistoryRepo.getAll();
|
||||
expect(itemsBefore).toHaveLength(0);
|
||||
|
||||
await sut.onBootstrap();
|
||||
|
||||
const itemsAfter = await repos.versionHistory.getAll();
|
||||
const itemsAfter = await versionHistoryRepo.getAll();
|
||||
expect(itemsAfter).toHaveLength(1);
|
||||
expect(itemsAfter[0]).toEqual({
|
||||
createdAt: expect.any(Date),
|
||||
@@ -43,22 +45,26 @@ describe(VersionService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue memory creation when upgrading from 1.128.0', async () => {
|
||||
const { sut, repos, mocks } = setup();
|
||||
mocks.job.queue.mockResolvedValue(void 0);
|
||||
const { sut, ctx } = setup();
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
const versionHistoryRepo = ctx.get(VersionHistoryRepository);
|
||||
jobMock.queue.mockResolvedValue(void 0);
|
||||
|
||||
await repos.versionHistory.create({ version: 'v1.128.0' });
|
||||
await versionHistoryRepo.create({ version: 'v1.128.0' });
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE });
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE });
|
||||
});
|
||||
|
||||
it('should not queue memory creation when upgrading from 1.129.0', async () => {
|
||||
const { sut, repos, mocks } = setup();
|
||||
const { sut, ctx } = setup();
|
||||
const jobMock = ctx.getMock(JobRepository);
|
||||
const versionHistoryRepo = ctx.get(VersionHistoryRepository);
|
||||
|
||||
await repos.versionHistory.create({ version: 'v1.129.0' });
|
||||
await versionHistoryRepo.create({ version: 'v1.129.0' });
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumAssetExifsV1, () => {
|
||||
it('should detect and sync the first album asset exif', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.AlbumAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync album asset exif for own user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not sync album asset exif for unrelated user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR });
|
||||
const { session } = await ctx.newSession({ userId: user3.id });
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
|
||||
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should backfill album assets exif when a user shares an album with you', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newExif({ assetId: asset1Owner.id, make: 'asset1Owner' });
|
||||
await wait(2);
|
||||
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset1User2.id, make: 'asset1User2' });
|
||||
await wait(2);
|
||||
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset2User2.id, make: 'asset2User2' });
|
||||
await wait(2);
|
||||
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset3User2.id, make: 'asset3User2' });
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
// ack initial album asset exif sync
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
// create a second album
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await Promise.all(
|
||||
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
|
||||
ctx.newAlbumAsset({ albumId: album2.id, assetId }),
|
||||
),
|
||||
);
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
// should backfill the album user
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset1Owner.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset1User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetExifBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset3User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
it('should detect and sync the first album asset', async () => {
|
||||
const originalFileName = 'firstAsset';
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({
|
||||
originalFileName,
|
||||
ownerId: user2.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
});
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
originalFileName,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
duration: asset.duration,
|
||||
},
|
||||
type: SyncEntityType.AlbumAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync album asset for own user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not sync album asset for unrelated user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user3.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR });
|
||||
const { session } = await ctx.newSession({ userId: user3.id });
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
|
||||
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should backfill album assets when a user shares an album with you', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await wait(2);
|
||||
const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await wait(2);
|
||||
const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await wait(2);
|
||||
const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await wait(2);
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
// ack initial album asset sync
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
// create a second album
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await Promise.all(
|
||||
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
|
||||
ctx.newAlbumAsset({ albumId: album2.id, assetId }),
|
||||
),
|
||||
);
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
// should backfill the album user
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset1Owner.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset1User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset3User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumToAssetsV1, () => {
|
||||
it('should detect and sync the first album to asset relation', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync album to asset for owned albums', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync the album to asset for shared albums', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync album to asset for an album owned by another user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should backfill album to assets when a user shares an album with you', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset: album1Asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const { asset: album2Asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
// Backfill album
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumAsset({ albumId: album2.id, assetId: album2Asset.id });
|
||||
await wait(2);
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album1.id,
|
||||
assetId: album1Asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
// ack initial album to asset sync
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
// add user to backfill album
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
// should backfill the album to asset relation
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album2.id,
|
||||
assetId: album2Asset.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumToAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumToAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted album to asset relation', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const albumRepo = ctx.get(AlbumRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await albumRepo.removeAssetIds(album.id, [asset.id]);
|
||||
await wait(2);
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await assetRepo.remove({ id: asset.id });
|
||||
await wait(2);
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a deleted album to asset relation when the album is deleted', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const albumRepo = ctx.get(AlbumRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumToAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await albumRepo.delete(album.id);
|
||||
await wait(2);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -20,198 +19,156 @@ beforeAll(async () => {
|
||||
|
||||
describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
it('should sync an album user with the correct properties', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
const { user } = await ctx.newUser();
|
||||
const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
albumId: albumUser.albumId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
userId: albumUser.userId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('owner', () => {
|
||||
it('should detect and sync a new shared user', async () => {
|
||||
const { auth, testSync, getRepository } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
albumId: albumUser.albumId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
userId: albumUser.userId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an updated shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const albumUserRepo = ctx.get(AlbumUserRepository);
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
albumId: albumUser.albumId,
|
||||
role: AlbumUserRole.VIEWER,
|
||||
userId: albumUser.usersId,
|
||||
userId: albumUser.userId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const albumUserRepo = ctx.get(AlbumUserRepository);
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
userId: albumUser.usersId,
|
||||
albumId: albumUser.albumId,
|
||||
userId: albumUser.userId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared user', () => {
|
||||
it('should detect and sync a new shared user', async () => {
|
||||
const { auth, testSync, getRepository } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: user1.id });
|
||||
const { albumUser } = await ctx.newAlbumUser({
|
||||
albumId: album.id,
|
||||
userId: auth.user.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
});
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
albumId: albumUser.albumId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
userId: albumUser.userId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an updated shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const albumUserRepo = ctx.get(AlbumUserRepository);
|
||||
const { user: owner } = await ctx.newUser();
|
||||
const { user: user } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: owner.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(2);
|
||||
|
||||
const owner = mediumFactory.userInsert();
|
||||
const user = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||
await albumRepo.create(
|
||||
album,
|
||||
[],
|
||||
[
|
||||
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||
],
|
||||
);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(2);
|
||||
const acks = [initialSyncResponse[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -222,39 +179,29 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const albumUserRepo = ctx.get(AlbumUserRepository);
|
||||
const { user: owner } = await ctx.newUser();
|
||||
const { user: user } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: owner.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const owner = mediumFactory.userInsert();
|
||||
const user = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||
await albumRepo.create(
|
||||
album,
|
||||
[],
|
||||
[
|
||||
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||
],
|
||||
);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(2);
|
||||
const acks = [initialSyncResponse[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(2);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -264,35 +211,27 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
type: SyncEntityType.AlbumUserDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should backfill album users when a user shares an album with you', async () => {
|
||||
const { auth, sut, testSync, getRepository } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album1 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
const album2 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
await albumRepo.create(album1, [], []);
|
||||
await albumRepo.create(album2, [], []);
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user1.id });
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user1.id });
|
||||
// backfill album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
await wait(2);
|
||||
// initial album user
|
||||
await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await wait(2);
|
||||
// post checkpoint album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: user2.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
@@ -307,15 +246,13 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
]);
|
||||
|
||||
// ack initial user
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
// get access to the backfill album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
// should backfill the album user
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(backfillResponse).toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -350,10 +287,8 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.setAcks(auth, { acks: [backfillResponse[1].ack, backfillResponse.at(-1).ack] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(finalResponse).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -20,11 +20,12 @@ beforeAll(async () => {
|
||||
|
||||
describe(SyncRequestType.AlbumsV1, () => {
|
||||
it('should sync an album with the correct properties', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const { auth, ctx } = await setup();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -35,14 +36,18 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a new album', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const { auth, ctx } = await setup();
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -51,14 +56,19 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album delete', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const { auth, ctx } = await setup();
|
||||
const albumRepo = ctx.get(AlbumRepository);
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
@@ -69,7 +79,10 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
]);
|
||||
|
||||
await albumRepo.delete(album.id);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
@@ -78,67 +91,60 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
describe('shared albums', () => {
|
||||
it('should detect and sync an album create', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const userRepo = getRepository('user');
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album share (share before sync)', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album share (share after sync)', async () => {
|
||||
const { auth, getRepository, sut, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album: userAlbum } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
const { album: user2Album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
const user2Album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
|
||||
expect(initialSyncResponse).toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: userAlbum.id }),
|
||||
@@ -146,75 +152,76 @@ describe(SyncRequestType.AlbumsV1, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: user2Album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album delete`', async () => {
|
||||
const { auth, getRepository, testSync, sut } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const userRepo = getRepository('user');
|
||||
const { auth, ctx } = await setup();
|
||||
const albumRepo = ctx.get(AlbumRepository);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
|
||||
await albumRepo.delete(album.id);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: { albumId: album.id },
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album unshare as an album delete', async () => {
|
||||
const { auth, getRepository, testSync, sut } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
const { auth, ctx } = await setup();
|
||||
const albumUserRepo = ctx.get(AlbumUserRepository);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: { albumId: album.id },
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,100 +1,78 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||
describe(SyncRequestType.AssetExifsV1, () => {
|
||||
it('should detect and sync the first asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.AssetExifV1,
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: SyncEntityType.AssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should only sync asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
const { session } = await ctx.newSession({ userId: user2.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||
describe(SyncEntityType.AssetV1, () => {
|
||||
it('should detect and sync the first asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const originalFileName = 'firstAsset';
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({
|
||||
originalFileName,
|
||||
ownerId: auth.user.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
@@ -40,96 +37,70 @@ describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
originalFileName,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
duration: asset.duration,
|
||||
},
|
||||
type: 'AssetV1',
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
originalFileName,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
duration: asset.duration,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'AssetV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: 'AssetDeleteV1',
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'AssetDeleteV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user2.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.MemoryToAssetV1, () => {
|
||||
it('should detect and sync a memory to asset relation', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { memory } = await ctx.newMemory({ ownerId: user.id });
|
||||
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
memoryId: memory.id,
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: 'MemoryToAssetV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted memory to asset relation', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { memory } = await ctx.newMemory({ ownerId: user.id });
|
||||
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id });
|
||||
await memoryRepo.removeAssetIds(memory.id, [asset.id]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
memoryId: memory.id,
|
||||
},
|
||||
type: 'MemoryToAssetDeleteV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a memory to asset relation or delete for an unrelated user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const { auth: auth2, user: user2 } = await ctx.newSyncAuthUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const { memory } = await ctx.newMemory({ ownerId: user2.id });
|
||||
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id });
|
||||
|
||||
expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0);
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1);
|
||||
|
||||
await memoryRepo.removeAssetIds(memory.id, [asset.id]);
|
||||
expect(await ctx.syncStream(auth, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(0);
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.MemoryToAssetsV1])).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.MemoryV1, () => {
|
||||
it('should detect and sync the first memory with the right properties', async () => {
|
||||
const { auth, user: user1, ctx } = await setup();
|
||||
const { memory } = await ctx.newMemory({ ownerId: user1.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: memory.id,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: memory.deletedAt,
|
||||
type: memory.type,
|
||||
data: memory.data,
|
||||
hideAt: memory.hideAt,
|
||||
showAt: memory.showAt,
|
||||
seenAt: memory.seenAt,
|
||||
memoryAt: expect.any(String),
|
||||
isSaved: memory.isSaved,
|
||||
ownerId: memory.ownerId,
|
||||
},
|
||||
type: 'MemoryV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted memory', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const { memory } = await ctx.newMemory({ ownerId: user.id });
|
||||
await memoryRepo.delete(memory.id);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
memoryId: memory.id,
|
||||
},
|
||||
type: 'MemoryDeleteV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync a memory and then an update to that same memory', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const { memory } = await ctx.newMemory({ ownerId: user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: memory.id }),
|
||||
type: 'MemoryV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await memoryRepo.update(memory.id, { seenAt: new Date() });
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.MemoriesV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: memory.id }),
|
||||
type: 'MemoryV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a memory or a memory delete for an unrelated user', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const memoryRepo = ctx.get(MemoryRepository);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { memory } = await ctx.newMemory({ ownerId: user2.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]);
|
||||
await memoryRepo.delete(memory.id);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.MemoriesV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -21,134 +19,89 @@ beforeAll(async () => {
|
||||
|
||||
describe(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
it('should detect and sync the first partner asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
const { session } = await ctx.newSession({ userId: user3.id });
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should backfill partner asset exif when a partner shared their library with you', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
await userRepo.create(user3);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(assetUser3);
|
||||
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'Canon' });
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: assetUser3.id, make: 'Canon' });
|
||||
await wait(2);
|
||||
await assetRepo.create(assetUser2);
|
||||
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'Canon' });
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: assetUser2.id, make: 'Canon' });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -162,189 +115,133 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(newResponse).toHaveLength(2);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(backfillResponse).toHaveLength(2);
|
||||
expect(backfillResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const backfillAck = backfillResponse[1].ack;
|
||||
await sut.setAcks(auth, { acks: [backfillAck] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
const finalAcks = finalResponse.map(({ ack }) => ack);
|
||||
expect(finalAcks).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle partners with users ids lower than a uuidv7', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' });
|
||||
const user3 = mediumFactory.userInsert({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' });
|
||||
await userRepo.create(user2);
|
||||
await userRepo.create(user3);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(assetUser3);
|
||||
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' });
|
||||
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' });
|
||||
const { user: user3 } = await ctx.newUser({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' });
|
||||
const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' });
|
||||
await wait(2);
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: assetUser2.id, make: 'assetUser2' });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(assetUser2);
|
||||
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' });
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
// This checks that our ack upsert is correct
|
||||
const ackUpsertResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(ackUpsertResponse).toEqual([]);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(newResponse).toHaveLength(2);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const syncAckResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(syncAckResponse).toHaveLength(2);
|
||||
expect(syncAckResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const syncAckResponseAcks = syncAckResponse.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks: [syncAckResponseAcks[1]] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(finalResponse).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
await userRepo.create(user3);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
const asset2User3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(assetUser3);
|
||||
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' });
|
||||
const { auth, ctx } = await setup();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' });
|
||||
await wait(2);
|
||||
await assetRepo.create(assetUser2);
|
||||
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' });
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newExif({ assetId: assetUser2.id, make: 'assetUser2' });
|
||||
await wait(2);
|
||||
await assetRepo.create(asset2User3);
|
||||
await assetRepo.upsertExif({ assetId: asset2User3.id, make: 'asset2User3' });
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newExif({ assetId: asset2User3.id, make: 'asset2User3' });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(newResponse).toHaveLength(3);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset2User3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(backfillResponse).toHaveLength(3);
|
||||
expect(backfillResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
assetId: asset2User3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const backfillAck = backfillResponse[1].ack;
|
||||
const partnerAssetAck = backfillResponse[2].ack;
|
||||
await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] });
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
const finalAcks = finalResponse.map(({ ack }) => ack);
|
||||
expect(finalAcks).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -21,19 +22,15 @@ beforeAll(async () => {
|
||||
|
||||
describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
it('should detect and sync the first partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const originalFileName = 'firstPartnerAsset';
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({
|
||||
ownerId: user2.id,
|
||||
originalFileName,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
@@ -44,315 +41,217 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
originalFileName,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
localDateTime: date,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
duration: asset.duration,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
originalFileName,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
localDateTime: date,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
duration: asset.duration,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
const { auth, ctx } = await setup();
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await ctx.newAsset({ ownerId: user2.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||
await partnerRepo.create(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||
const { auth, ctx } = await setup();
|
||||
const partnerRepo = ctx.get(PartnerRepository);
|
||||
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newAsset({ ownerId: user2.id });
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
const { auth, ctx } = await setup();
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user2.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should backfill partner assets when a partner shared their library with you', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
await userRepo.create(user3);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(assetUser3);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await wait(2);
|
||||
await assetRepo.create(assetUser2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(newResponse).toHaveLength(2);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(backfillResponse).toHaveLength(2);
|
||||
expect(backfillResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const backfillAck = backfillResponse[1].ack;
|
||||
await sut.setAcks(auth, { acks: [backfillAck] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
const finalAcks = finalResponse.map(({ ack }) => ack);
|
||||
expect(finalAcks).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
await userRepo.create(user3);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
const asset2User3 = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(assetUser3);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await wait(2);
|
||||
await assetRepo.create(assetUser2);
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await wait(2);
|
||||
await assetRepo.create(asset2User3);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(newResponse).toHaveLength(3);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset2User3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(backfillResponse).toHaveLength(3);
|
||||
expect(backfillResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset2User3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const backfillAck = backfillResponse[1].ack;
|
||||
const partnerAssetAck = backfillResponse[2].ack;
|
||||
await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
const finalAcks = finalResponse.map(({ ack }) => ack);
|
||||
expect(finalAcks).toEqual([]);
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,75 +1,58 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
describe(SyncEntityType.PartnerV1, () => {
|
||||
it('should detect and sync the first partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, user: user1, ctx } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const userRepo = getRepository('user');
|
||||
const partnerRepo = getRepository('partner');
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, user: user1, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const partnerRepo = ctx.get(PartnerRepository);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -84,27 +67,18 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a partner share both to and from another user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, user: user1, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { partner: partner1 } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -129,93 +103,80 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
]),
|
||||
);
|
||||
|
||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync a partner and then an update to that same partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
const { auth, user: user1, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const partnerRepo = ctx.get(PartnerRepository);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
const updated = await partnerRepo.update(
|
||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: updated.inTimeline,
|
||||
sharedById: updated.sharedById,
|
||||
sharedWithId: updated.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: updated.inTimeline,
|
||||
sharedById: updated.sharedById,
|
||||
sharedWithId: updated.sharedWithId,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||
const partnerRepo = ctx.get(PartnerRepository);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { user: user3 } = await ctx.newUser();
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should not sync a partner delete after a user is deleted', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,4 +9,11 @@ describe('types', () => {
|
||||
|
||||
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||
});
|
||||
|
||||
it('should ensure album follows albums assets', () => {
|
||||
const albumIndex = SYNC_TYPES_ORDER.indexOf(SyncRequestType.AlbumsV1);
|
||||
const albumAssetsIndex = SYNC_TYPES_ORDER.indexOf(SyncRequestType.AlbumAssetsV1);
|
||||
|
||||
expect(albumIndex).toBeGreaterThan(albumAssetsIndex);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
describe(SyncEntityType.UserV1, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||
if (!user) {
|
||||
expect.fail('First user should exist');
|
||||
}
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual([
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
@@ -43,21 +42,17 @@ describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a soft deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||
const deleted = await getRepository('user').create(deletedUser);
|
||||
const { user: deleted } = await ctx.newUser({ deletedAt });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
@@ -85,95 +80,81 @@ describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [response[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
const { user } = await ctx.newUser();
|
||||
await userRepo.delete({ id: user.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
type: 'UserDeleteV1',
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
type: 'UserDeleteV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should sync a user and then an update to that same user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
const { auth, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: updated.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
|
||||
expect(newResponse).toHaveLength(1);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: updated.name,
|
||||
},
|
||||
]),
|
||||
);
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepository>> => {
|
||||
return {
|
||||
reassignFaces: vitest.fn(),
|
||||
unassignFaces: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
deleteFaces: vitest.fn(),
|
||||
getAllFaces: vitest.fn(),
|
||||
getAll: vitest.fn(),
|
||||
getAllForUser: vitest.fn(),
|
||||
getAllWithoutFaces: vitest.fn(),
|
||||
getFaces: vitest.fn(),
|
||||
getFaceById: vitest.fn(),
|
||||
getFaceForFacialRecognitionJob: vitest.fn(),
|
||||
getDataForThumbnailGenerationJob: vitest.fn(),
|
||||
reassignFace: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
getByName: vitest.fn(),
|
||||
getDistinctNames: vitest.fn(),
|
||||
getStatistics: vitest.fn(),
|
||||
getNumberOfPeople: vitest.fn(),
|
||||
create: vitest.fn(),
|
||||
createAll: vitest.fn(),
|
||||
refreshFaces: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
updateAll: vitest.fn(),
|
||||
getFacesByIds: vitest.fn(),
|
||||
getRandomFace: vitest.fn(),
|
||||
getLatestFaceDate: vitest.fn(),
|
||||
createAssetFace: vitest.fn(),
|
||||
deleteAssetFace: vitest.fn(),
|
||||
softDeleteAssetFaces: vitest.fn(),
|
||||
vacuum: vitest.fn(),
|
||||
};
|
||||
};
|
||||
@@ -47,6 +47,7 @@ import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
@@ -67,7 +68,6 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
@@ -218,6 +218,7 @@ export type ServiceOverrides = {
|
||||
stack: StackRepository;
|
||||
storage: StorageRepository;
|
||||
sync: SyncRepository;
|
||||
syncCheckpoint: SyncCheckpointRepository;
|
||||
systemMetadata: SystemMetadataRepository;
|
||||
tag: TagRepository;
|
||||
telemetry: TelemetryRepository;
|
||||
@@ -278,7 +279,7 @@ export const newTestService = <T extends BaseService>(
|
||||
notification: automock(NotificationRepository),
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: newPersonRepositoryMock(),
|
||||
person: automock(PersonRepository, { strict: false }),
|
||||
process: automock(ProcessRepository),
|
||||
search: automock(SearchRepository, { strict: false }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
@@ -288,6 +289,7 @@ export const newTestService = <T extends BaseService>(
|
||||
stack: automock(StackRepository),
|
||||
storage: newStorageRepositoryMock(),
|
||||
sync: automock(SyncRepository),
|
||||
syncCheckpoint: automock(SyncCheckpointRepository),
|
||||
systemMetadata: newSystemMetadataRepositoryMock(),
|
||||
// systemMetadata: automock(SystemMetadataRepository, { strict: false }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
@@ -337,6 +339,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.stack || (mocks.stack as As<StackRepository>),
|
||||
overrides.storage || (mocks.storage as As<StorageRepository>),
|
||||
overrides.sync || (mocks.sync as As<SyncRepository>),
|
||||
overrides.syncCheckpoint || (mocks.syncCheckpoint as As<SyncCheckpointRepository>),
|
||||
overrides.systemMetadata || (mocks.systemMetadata as As<SystemMetadataRepository>),
|
||||
overrides.tag || (mocks.tag as As<TagRepository>),
|
||||
overrides.telemetry || (mocks.telemetry as unknown as TelemetryRepository),
|
||||
|
||||
Reference in New Issue
Block a user