2025-06-26 15:32:06 -04:00
|
|
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
2025-04-15 08:53:14 -04:00
|
|
|
import { Insertable, Kysely } from 'kysely';
|
|
|
|
import { DateTime } from 'luxon';
|
2025-04-15 15:54:23 -04:00
|
|
|
import { createHash, randomBytes } from 'node:crypto';
|
|
|
|
import { Writable } from 'node:stream';
|
|
|
|
import { AssetFace } from 'src/database';
|
2025-06-27 15:35:19 -04:00
|
|
|
import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto';
|
2025-06-27 12:20:13 -04:00
|
|
|
import { AlbumUserRole, AssetType, AssetVisibility, MemoryType, SourceType, SyncRequestType } from 'src/enum';
|
2025-06-25 11:12:36 -04:00
|
|
|
import { AccessRepository } from 'src/repositories/access.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
2025-05-21 15:35:32 -04:00
|
|
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
2025-04-15 10:24:51 -04:00
|
|
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
|
|
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
|
|
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
|
|
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
2025-04-28 10:36:14 -04:00
|
|
|
import { EmailRepository } from 'src/repositories/email.repository';
|
2025-06-27 15:35:19 -04:00
|
|
|
import { EventRepository } from 'src/repositories/event.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { JobRepository } from 'src/repositories/job.repository';
|
|
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
|
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
2025-04-28 10:36:14 -04:00
|
|
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
2025-04-15 15:54:23 -04:00
|
|
|
import { PersonRepository } from 'src/repositories/person.repository';
|
|
|
|
import { SearchRepository } from 'src/repositories/search.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { SessionRepository } from 'src/repositories/session.repository';
|
2025-06-30 15:26:41 -04:00
|
|
|
import { StackRepository } from 'src/repositories/stack.repository';
|
2025-06-25 11:12:36 -04:00
|
|
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
2025-06-27 13:47:06 -04:00
|
|
|
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
2025-04-15 15:54:23 -04:00
|
|
|
import { SyncRepository } from 'src/repositories/sync.repository';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
|
|
|
import { UserRepository } from 'src/repositories/user.repository';
|
|
|
|
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
2025-06-30 13:19:16 -04:00
|
|
|
import { DB } from 'src/schema';
|
|
|
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
|
|
|
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
|
|
|
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
|
|
|
import { ExifTable } from 'src/schema/tables/exif.table';
|
|
|
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
|
|
|
import { MemoryTable } from 'src/schema/tables/memory.table';
|
|
|
|
import { PersonTable } from 'src/schema/tables/person.table';
|
|
|
|
import { SessionTable } from 'src/schema/tables/session.table';
|
2025-06-30 15:26:41 -04:00
|
|
|
import { StackTable } from 'src/schema/tables/stack.table';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { UserTable } from 'src/schema/tables/user.table';
|
2025-06-26 15:32:06 -04:00
|
|
|
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
2025-05-21 15:35:32 -04:00
|
|
|
import { SyncService } from 'src/services/sync.service';
|
|
|
|
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
2025-06-26 15:32:06 -04:00
|
|
|
import { automock, wait } from 'test/utils';
|
2025-04-15 08:53:14 -04:00
|
|
|
import { Mocked } from 'vitest';
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
interface ClassConstructor<T = any> extends Function {
|
|
|
|
new (...args: any[]): T;
|
|
|
|
}
|
2025-04-15 15:54:23 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
type MediumTestOptions = {
|
|
|
|
mock: ClassConstructor<any>[];
|
|
|
|
real: ClassConstructor<any>[];
|
|
|
|
database: Kysely<DB>;
|
2025-04-15 08:53:14 -04:00
|
|
|
};
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
export const newMediumService = <S extends BaseService>(Service: ClassConstructor<S>, options: MediumTestOptions) => {
|
|
|
|
const ctx = new MediumTestContext(Service, options);
|
|
|
|
return { sut: ctx.sut, ctx };
|
2025-04-15 08:53:14 -04:00
|
|
|
};
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
export class MediumTestContext<S extends BaseService = BaseService> {
|
|
|
|
private repoCache: Record<string, any> = {};
|
|
|
|
private sutDeps: any[];
|
2025-04-15 08:53:14 -04:00
|
|
|
|
|
|
|
sut: S;
|
2025-06-26 15:32:06 -04:00
|
|
|
database: Kysely<DB>;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
Service: ClassConstructor<S>,
|
|
|
|
private options: MediumTestOptions,
|
|
|
|
) {
|
|
|
|
this.sutDeps = this.makeDeps(options);
|
|
|
|
this.sut = new Service(...this.sutDeps);
|
|
|
|
this.database = options.database;
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
private makeDeps(options: MediumTestOptions) {
|
|
|
|
const deps = BASE_SERVICE_DEPENDENCIES;
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
for (const dep of options.mock) {
|
|
|
|
if (!deps.includes(dep)) {
|
|
|
|
throw new Error(`Mocked repository ${dep.name} is not a valid dependency`);
|
|
|
|
}
|
2025-06-25 11:12:36 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
for (const dep of options.real) {
|
|
|
|
if (!deps.includes(dep)) {
|
|
|
|
throw new Error(`Real repository ${dep.name} is not a valid dependency`);
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
2025-06-26 15:32:06 -04:00
|
|
|
return (deps as ClassConstructor<any>[]).map((dep) => {
|
|
|
|
if (options.real.includes(dep)) {
|
|
|
|
return this.get(dep);
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
if (options.mock.includes(dep)) {
|
|
|
|
return newMockRepository(dep);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2025-05-21 15:35:32 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
get<T>(key: ClassConstructor<T>): T {
|
|
|
|
if (!this.repoCache[key.name]) {
|
|
|
|
const real = newRealRepository(key, this.options.database);
|
|
|
|
this.repoCache[key.name] = real;
|
2025-05-21 15:35:32 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
return this.repoCache[key.name];
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
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}`);
|
2025-04-15 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
return this.sutDeps[index] as R;
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
async newUser(dto: Partial<Insertable<UserTable>> = {}) {
|
|
|
|
const user = mediumFactory.userInsert(dto);
|
|
|
|
const result = await this.get(UserRepository).create(user);
|
|
|
|
return { user, result };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
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 };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-30 15:26:41 -04:00
|
|
|
async newStack(dto: Omit<Insertable<StackTable>, 'primaryAssetId'>, assetIds: string[]) {
|
|
|
|
const date = factory.date();
|
|
|
|
const stack = {
|
|
|
|
id: factory.uuid(),
|
|
|
|
createdAt: date,
|
|
|
|
updatedAt: date,
|
|
|
|
...dto,
|
|
|
|
};
|
|
|
|
|
|
|
|
const result = await this.get(StackRepository).create(stack, assetIds);
|
|
|
|
return { stack: { ...stack, primaryAssetId: assetIds[0] }, result };
|
|
|
|
}
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newAsset(dto: Partial<Insertable<AssetTable>> = {}) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const asset = mediumFactory.assetInsert(dto);
|
|
|
|
const result = await this.get(AssetRepository).create(asset);
|
|
|
|
return { asset, result };
|
|
|
|
}
|
2025-04-28 10:36:14 -04:00
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newMemory(dto: Partial<Insertable<MemoryTable>> = {}) {
|
2025-06-27 12:20:13 -04:00
|
|
|
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 };
|
|
|
|
}
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newExif(dto: Insertable<ExifTable>) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const result = await this.get(AssetRepository).upsertExif(dto);
|
|
|
|
return { result };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newAlbum(dto: Insertable<AlbumTable>) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const album = mediumFactory.albumInsert(dto);
|
|
|
|
const result = await this.get(AlbumRepository).create(album, [], []);
|
|
|
|
return { album, result };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
async newAlbumAsset(albumAsset: { albumId: string; assetId: string }) {
|
|
|
|
const result = await this.get(AlbumRepository).addAssetIds(albumAsset.albumId, [albumAsset.assetId]);
|
|
|
|
return { albumAsset, result };
|
|
|
|
}
|
2025-04-28 10:36:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
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 };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newJobStatus(dto: Partial<Insertable<AssetJobStatusTable>> & { assetId: string }) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId });
|
|
|
|
const result = await this.get(AssetRepository).upsertJobStatus(jobStatus);
|
|
|
|
return { jobStatus, result };
|
|
|
|
}
|
2025-04-15 15:54:23 -04:00
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newPerson(dto: Partial<Insertable<PersonTable>> & { ownerId: string }) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const person = mediumFactory.personInsert(dto);
|
|
|
|
const result = await this.get(PersonRepository).create(person);
|
|
|
|
return { person, result };
|
|
|
|
}
|
2025-04-15 15:54:23 -04:00
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
async newSession(dto: Partial<Insertable<SessionTable>> & { userId: string }) {
|
2025-06-26 15:32:06 -04:00
|
|
|
const session = mediumFactory.sessionInsert(dto);
|
|
|
|
const result = await this.get(SessionRepository).create(session);
|
|
|
|
return { session, result };
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2025-04-15 15:54:23 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
export class SyncTestContext extends MediumTestContext<SyncService> {
|
|
|
|
constructor(database: Kysely<DB>) {
|
2025-06-27 13:47:06 -04:00
|
|
|
super(SyncService, {
|
|
|
|
database,
|
|
|
|
real: [SyncRepository, SyncCheckpointRepository, SessionRepository],
|
|
|
|
mock: [LoggingRepository],
|
|
|
|
});
|
2025-06-26 15:32:06 -04:00
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
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 this.sut.stream(auth, stream, { types });
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
return stream.getResponse();
|
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
|
|
|
|
const acks: Record<string, string> = {};
|
|
|
|
for (const { type, ack } of response) {
|
|
|
|
acks[type] = ack;
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
2025-06-26 15:32:06 -04:00
|
|
|
|
|
|
|
await this.sut.setAcks(auth, { acks: Object.values(acks) });
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
2025-06-26 15:32:06 -04:00
|
|
|
}
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
2025-04-15 08:53:14 -04:00
|
|
|
switch (key) {
|
2025-06-26 15:32:06 -04:00
|
|
|
case AccessRepository:
|
|
|
|
case AlbumRepository:
|
|
|
|
case AlbumUserRepository:
|
|
|
|
case ActivityRepository:
|
|
|
|
case AssetRepository:
|
|
|
|
case AssetJobRepository:
|
|
|
|
case MemoryRepository:
|
|
|
|
case NotificationRepository:
|
|
|
|
case PartnerRepository:
|
|
|
|
case PersonRepository:
|
|
|
|
case SearchRepository:
|
|
|
|
case SessionRepository:
|
2025-06-30 15:26:41 -04:00
|
|
|
case StackRepository:
|
2025-06-26 15:32:06 -04:00
|
|
|
case SyncRepository:
|
2025-06-27 13:47:06 -04:00
|
|
|
case SyncCheckpointRepository:
|
2025-06-26 15:32:06 -04:00
|
|
|
case SystemMetadataRepository:
|
|
|
|
case UserRepository:
|
|
|
|
case VersionHistoryRepository: {
|
|
|
|
return new key(db);
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case ConfigRepository:
|
|
|
|
case CryptoRepository: {
|
|
|
|
return new key();
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case DatabaseRepository: {
|
|
|
|
return new key(db, LoggingRepository.create(), new ConfigRepository());
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case EmailRepository: {
|
|
|
|
return new key(LoggingRepository.create());
|
2025-04-15 10:24:51 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case LoggingRepository as unknown as ClassConstructor<LoggingRepository>: {
|
|
|
|
return new key() as unknown as T;
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
default: {
|
|
|
|
throw new Error(`Unable to create repository instance for key: ${key?.name || key}`);
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
2025-06-26 15:32:06 -04:00
|
|
|
}
|
|
|
|
};
|
2025-04-15 08:53:14 -04:00
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
|
|
|
switch (key) {
|
|
|
|
case ActivityRepository:
|
|
|
|
case AlbumRepository:
|
|
|
|
case AssetRepository:
|
|
|
|
case AssetJobRepository:
|
|
|
|
case ConfigRepository:
|
|
|
|
case CryptoRepository:
|
|
|
|
case MemoryRepository:
|
|
|
|
case NotificationRepository:
|
|
|
|
case PartnerRepository:
|
|
|
|
case PersonRepository:
|
|
|
|
case SessionRepository:
|
|
|
|
case SyncRepository:
|
2025-06-27 13:47:06 -04:00
|
|
|
case SyncCheckpointRepository:
|
2025-06-26 15:32:06 -04:00
|
|
|
case SystemMetadataRepository:
|
|
|
|
case UserRepository:
|
|
|
|
case VersionHistoryRepository: {
|
|
|
|
return automock(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
case DatabaseRepository: {
|
2025-04-15 08:53:14 -04:00
|
|
|
return automock(DatabaseRepository, {
|
2025-06-26 15:32:06 -04:00
|
|
|
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
|
2025-04-15 08:53:14 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case EmailRepository: {
|
|
|
|
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
|
2025-04-28 10:36:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-27 15:35:19 -04:00
|
|
|
case EventRepository: {
|
|
|
|
return automock(EventRepository, { args: [undefined, undefined, { setContext: () => {} }] });
|
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case JobRepository: {
|
2025-05-06 12:12:48 -05:00
|
|
|
return automock(JobRepository, {
|
|
|
|
args: [
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
{
|
|
|
|
setContext: () => {},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
2025-04-15 08:53:14 -04:00
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case LoggingRepository as unknown as ClassConstructor<T>: {
|
2025-04-15 08:53:14 -04:00
|
|
|
const configMock = { getEnv: () => ({ noColor: false }) };
|
|
|
|
return automock(LoggingRepository, { args: [undefined, configMock], strict: false });
|
|
|
|
}
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
case StorageRepository: {
|
2025-06-25 11:12:36 -04:00
|
|
|
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
|
|
|
|
}
|
|
|
|
|
2025-04-15 08:53:14 -04:00
|
|
|
default: {
|
|
|
|
throw new Error(`Invalid repository key: ${key}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
|
2025-04-15 08:53:14 -04:00
|
|
|
const id = asset.id || newUuid();
|
2025-04-15 10:24:51 -04:00
|
|
|
const now = newDate();
|
2025-06-30 13:19:16 -04:00
|
|
|
const defaults: Insertable<AssetTable> = {
|
2025-04-15 08:53:14 -04:00
|
|
|
deviceAssetId: '',
|
|
|
|
deviceId: '',
|
|
|
|
originalFileName: '',
|
|
|
|
checksum: randomBytes(32),
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
originalPath: '/path/to/something.jpg',
|
|
|
|
ownerId: '@immich.cloud',
|
2025-04-15 15:54:23 -04:00
|
|
|
isFavorite: false,
|
2025-04-15 10:24:51 -04:00
|
|
|
fileCreatedAt: now,
|
|
|
|
fileModifiedAt: now,
|
|
|
|
localDateTime: now,
|
2025-05-06 12:12:48 -05:00
|
|
|
visibility: AssetVisibility.TIMELINE,
|
2025-04-15 08:53:14 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...asset,
|
|
|
|
id,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const albumInsert = (album: Partial<Insertable<AlbumTable>> & { ownerId: string }) => {
|
2025-05-21 15:35:32 -04:00
|
|
|
const id = album.id || newUuid();
|
2025-06-30 13:19:16 -04:00
|
|
|
const defaults: Omit<Insertable<AlbumTable>, 'ownerId'> = {
|
2025-05-21 15:35:32 -04:00
|
|
|
albumName: 'Album',
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...album,
|
|
|
|
id,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const faceInsert = (face: Partial<Insertable<FaceSearchTable>> & { faceId: string }) => {
|
2025-04-15 15:54:23 -04:00
|
|
|
const defaults = {
|
|
|
|
faceId: face.faceId,
|
|
|
|
embedding: face.embedding || newEmbedding(),
|
|
|
|
};
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...face,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const assetFaceInsert = (assetFace: Partial<AssetFace> & { assetId: string }) => {
|
|
|
|
const defaults = {
|
|
|
|
assetId: assetFace.assetId ?? newUuid(),
|
|
|
|
boundingBoxX1: assetFace.boundingBoxX1 ?? 0,
|
|
|
|
boundingBoxX2: assetFace.boundingBoxX2 ?? 1,
|
|
|
|
boundingBoxY1: assetFace.boundingBoxY1 ?? 0,
|
|
|
|
boundingBoxY2: assetFace.boundingBoxY2 ?? 1,
|
|
|
|
deletedAt: assetFace.deletedAt ?? null,
|
|
|
|
id: assetFace.id ?? newUuid(),
|
|
|
|
imageHeight: assetFace.imageHeight ?? 10,
|
|
|
|
imageWidth: assetFace.imageWidth ?? 10,
|
|
|
|
personId: assetFace.personId ?? null,
|
|
|
|
sourceType: assetFace.sourceType ?? SourceType.MACHINE_LEARNING,
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...assetFace,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-04-15 08:53:14 -04:00
|
|
|
const assetJobStatusInsert = (
|
2025-06-30 13:19:16 -04:00
|
|
|
job: Partial<Insertable<AssetJobStatusTable>> & { assetId: string },
|
|
|
|
): Insertable<AssetJobStatusTable> => {
|
2025-04-15 08:53:14 -04:00
|
|
|
const date = DateTime.now().minus({ days: 15 }).toISO();
|
2025-06-30 13:19:16 -04:00
|
|
|
const defaults: Omit<Insertable<AssetJobStatusTable>, 'assetId'> = {
|
2025-04-15 08:53:14 -04:00
|
|
|
duplicatesDetectedAt: date,
|
|
|
|
facesRecognizedAt: date,
|
|
|
|
metadataExtractedAt: date,
|
|
|
|
previewAt: date,
|
|
|
|
thumbnailAt: date,
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...job,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const personInsert = (person: Partial<Insertable<PersonTable>> & { ownerId: string }) => {
|
2025-04-15 15:54:23 -04:00
|
|
|
const defaults = {
|
|
|
|
birthDate: person.birthDate || null,
|
|
|
|
color: person.color || null,
|
|
|
|
createdAt: person.createdAt || newDate(),
|
|
|
|
faceAssetId: person.faceAssetId || null,
|
|
|
|
id: person.id || newUuid(),
|
|
|
|
isFavorite: person.isFavorite || false,
|
|
|
|
isHidden: person.isHidden || false,
|
|
|
|
name: person.name || 'Test Name',
|
|
|
|
ownerId: person.ownerId || newUuid(),
|
|
|
|
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
|
|
|
|
updatedAt: person.updatedAt || newDate(),
|
|
|
|
updateId: person.updateId || newUuid(),
|
|
|
|
};
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...person,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-06-26 15:32:06 -04:00
|
|
|
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const sessionInsert = ({
|
|
|
|
id = newUuid(),
|
|
|
|
userId,
|
|
|
|
...session
|
|
|
|
}: Partial<Insertable<SessionTable>> & { userId: string }) => {
|
|
|
|
const defaults: Insertable<SessionTable> = {
|
2025-04-15 15:54:23 -04:00
|
|
|
id,
|
|
|
|
userId,
|
|
|
|
token: sha256(id),
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
...defaults,
|
|
|
|
...session,
|
|
|
|
id,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-04-15 08:53:14 -04:00
|
|
|
const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
|
|
|
|
const id = user.id || newUuid();
|
|
|
|
|
2025-06-27 15:35:19 -04:00
|
|
|
const defaults = {
|
2025-04-15 08:53:14 -04:00
|
|
|
email: `${id}@immich.cloud`,
|
|
|
|
name: `User ${id}`,
|
|
|
|
deletedAt: null,
|
2025-06-27 15:35:19 -04:00
|
|
|
isAdmin: false,
|
|
|
|
profileImagePath: '',
|
|
|
|
shouldChangePassword: true,
|
2025-04-15 08:53:14 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return { ...defaults, ...user, id };
|
|
|
|
};
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const memoryInsert = (memory: Partial<Insertable<MemoryTable>> = {}) => {
|
2025-06-27 12:20:13 -04:00
|
|
|
const id = memory.id || newUuid();
|
|
|
|
const date = newDate();
|
|
|
|
|
2025-06-30 13:19:16 -04:00
|
|
|
const defaults: Insertable<MemoryTable> = {
|
2025-06-27 12:20:13 -04:00
|
|
|
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 };
|
|
|
|
};
|
|
|
|
|
2025-04-15 15:54:23 -04:00
|
|
|
class CustomWritable extends Writable {
|
|
|
|
private data = '';
|
|
|
|
|
|
|
|
_write(chunk: any, encoding: string, callback: () => void) {
|
|
|
|
this.data += chunk.toString();
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
getResponse() {
|
|
|
|
const result = this.data;
|
|
|
|
return result
|
|
|
|
.split('\n')
|
|
|
|
.filter((x) => x.length > 0)
|
|
|
|
.map((x) => JSON.parse(x));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const syncStream = () => {
|
|
|
|
return new CustomWritable();
|
|
|
|
};
|
|
|
|
|
2025-06-27 15:35:19 -04:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2025-04-15 08:53:14 -04:00
|
|
|
export const mediumFactory = {
|
|
|
|
assetInsert,
|
2025-04-15 15:54:23 -04:00
|
|
|
assetFaceInsert,
|
2025-04-15 08:53:14 -04:00
|
|
|
assetJobStatusInsert,
|
2025-05-21 15:35:32 -04:00
|
|
|
albumInsert,
|
2025-04-15 15:54:23 -04:00
|
|
|
faceInsert,
|
|
|
|
personInsert,
|
|
|
|
sessionInsert,
|
|
|
|
syncStream,
|
2025-04-15 08:53:14 -04:00
|
|
|
userInsert,
|
2025-06-27 12:20:13 -04:00
|
|
|
memoryInsert,
|
2025-06-27 15:35:19 -04:00
|
|
|
loginDetails,
|
|
|
|
loginResponse,
|
2025-04-15 08:53:14 -04:00
|
|
|
};
|