mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			592 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			592 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { ClassConstructor } from 'class-transformer';
 | 
						|
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 { ActivityRepository } from 'src/repositories/activity.repository';
 | 
						|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
 | 
						|
import { AlbumRepository } from 'src/repositories/album.repository';
 | 
						|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
 | 
						|
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';
 | 
						|
import { EmailRepository } from 'src/repositories/email.repository';
 | 
						|
import { JobRepository } from 'src/repositories/job.repository';
 | 
						|
import { LoggingRepository } from 'src/repositories/logging.repository';
 | 
						|
import { MemoryRepository } from 'src/repositories/memory.repository';
 | 
						|
import { NotificationRepository } from 'src/repositories/notification.repository';
 | 
						|
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 { 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 { SyncService } from 'src/services/sync.service';
 | 
						|
import { RepositoryInterface } from 'src/types';
 | 
						|
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
 | 
						|
import { automock, ServiceOverrides } from 'test/utils';
 | 
						|
import { Mocked } from 'vitest';
 | 
						|
 | 
						|
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
 | 
						|
 | 
						|
// 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 ContextRepositories<R extends RepositoryOptions> = {
 | 
						|
  [K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K];
 | 
						|
};
 | 
						|
 | 
						|
export type Context<R extends RepositoryOptions, S extends BaseService> = {
 | 
						|
  sut: S;
 | 
						|
  mocks: ContextRepositoryMocks<R>;
 | 
						|
  repos: ContextRepositories<R>;
 | 
						|
  getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
 | 
						|
};
 | 
						|
 | 
						|
export type SyncTestOptions = {
 | 
						|
  db: Kysely<DB>;
 | 
						|
};
 | 
						|
 | 
						|
export const newSyncAuthUser = () => {
 | 
						|
  const user = mediumFactory.userInsert();
 | 
						|
  const session = mediumFactory.sessionInsert({ userId: user.id });
 | 
						|
 | 
						|
  const auth = factory.auth({
 | 
						|
    session,
 | 
						|
    user: {
 | 
						|
      id: user.id,
 | 
						|
      name: user.name,
 | 
						|
      email: user.email,
 | 
						|
    },
 | 
						|
  });
 | 
						|
 | 
						|
  return {
 | 
						|
    auth,
 | 
						|
    session,
 | 
						|
    user,
 | 
						|
    create: async (db: Kysely<DB>) => {
 | 
						|
      await new UserRepository(db).create(user);
 | 
						|
      await new SessionRepository(db).create(session);
 | 
						|
    },
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
export const newSyncTest = (options: SyncTestOptions) => {
 | 
						|
  const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
 | 
						|
    database: options.db,
 | 
						|
    repos: {
 | 
						|
      sync: 'real',
 | 
						|
      session: 'real',
 | 
						|
    },
 | 
						|
  });
 | 
						|
 | 
						|
  const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
 | 
						|
    const stream = mediumFactory.syncStream();
 | 
						|
    // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
 | 
						|
    await new Promise((resolve) => setTimeout(resolve, 2));
 | 
						|
    await 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);
 | 
						|
  };
 | 
						|
 | 
						|
  const deps = asDeps({ ...mocks, ...repos } as ServiceOverrides);
 | 
						|
  const sut = new Service(...deps);
 | 
						|
 | 
						|
  return {
 | 
						|
    sut,
 | 
						|
    mocks,
 | 
						|
    repos,
 | 
						|
    getRepository: makeRepository,
 | 
						|
  } as Context<R, S>;
 | 
						|
};
 | 
						|
 | 
						|
export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
 | 
						|
  switch (key) {
 | 
						|
    case 'activity': {
 | 
						|
      return new ActivityRepository(db);
 | 
						|
    }
 | 
						|
 | 
						|
    case 'album': {
 | 
						|
      return new AlbumRepository(db);
 | 
						|
    }
 | 
						|
 | 
						|
    case 'albumUser': {
 | 
						|
      return new AlbumUserRepository(db);
 | 
						|
    }
 | 
						|
 | 
						|
    case 'asset': {
 | 
						|
      return new AssetRepository(db);
 | 
						|
    }
 | 
						|
 | 
						|
    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);
 | 
						|
    }
 | 
						|
 | 
						|
    default: {
 | 
						|
      throw new Error(`Invalid repository key: ${key}`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
 | 
						|
  switch (key) {
 | 
						|
    case 'activity': {
 | 
						|
      return automock(ActivityRepository) as Mocked<RepositoryInterface<ActivityRepository>>;
 | 
						|
    }
 | 
						|
 | 
						|
    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': {
 | 
						|
      return automock(DatabaseRepository, {
 | 
						|
        args: [
 | 
						|
          undefined,
 | 
						|
          {
 | 
						|
            setContext: () => {},
 | 
						|
          },
 | 
						|
          { getEnv: () => ({ database: { vectorExtension: '' } }) },
 | 
						|
        ],
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    case 'email': {
 | 
						|
      return automock(EmailRepository, {
 | 
						|
        args: [
 | 
						|
          {
 | 
						|
            setContext: () => {},
 | 
						|
          },
 | 
						|
        ],
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    case 'job': {
 | 
						|
      return automock(JobRepository, {
 | 
						|
        args: [
 | 
						|
          undefined,
 | 
						|
          undefined,
 | 
						|
          undefined,
 | 
						|
          {
 | 
						|
            setContext: () => {},
 | 
						|
          },
 | 
						|
        ],
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    case 'logger': {
 | 
						|
      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);
 | 
						|
    }
 | 
						|
 | 
						|
    default: {
 | 
						|
      throw new Error(`Invalid repository key: ${key}`);
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
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.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();
 | 
						|
  const defaults: Insertable<Assets> = {
 | 
						|
    deviceAssetId: '',
 | 
						|
    deviceId: '',
 | 
						|
    originalFileName: '',
 | 
						|
    checksum: randomBytes(32),
 | 
						|
    type: AssetType.IMAGE,
 | 
						|
    originalPath: '/path/to/something.jpg',
 | 
						|
    ownerId: '@immich.cloud',
 | 
						|
    isFavorite: false,
 | 
						|
    fileCreatedAt: now,
 | 
						|
    fileModifiedAt: now,
 | 
						|
    localDateTime: now,
 | 
						|
    visibility: AssetVisibility.TIMELINE,
 | 
						|
  };
 | 
						|
 | 
						|
  return {
 | 
						|
    ...defaults,
 | 
						|
    ...asset,
 | 
						|
    id,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const albumInsert = (album: Partial<Insertable<Albums>> & { ownerId: string }) => {
 | 
						|
  const id = album.id || newUuid();
 | 
						|
  const defaults: Omit<Insertable<Albums>, 'ownerId'> = {
 | 
						|
    albumName: 'Album',
 | 
						|
  };
 | 
						|
 | 
						|
  return {
 | 
						|
    ...defaults,
 | 
						|
    ...album,
 | 
						|
    id,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const faceInsert = (face: Partial<Insertable<FaceSearch>> & { faceId: string }) => {
 | 
						|
  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,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const assetJobStatusInsert = (
 | 
						|
  job: Partial<Insertable<AssetJobStatus>> & { assetId: string },
 | 
						|
): Insertable<AssetJobStatus> => {
 | 
						|
  const date = DateTime.now().minus({ days: 15 }).toISO();
 | 
						|
  const defaults: Omit<Insertable<AssetJobStatus>, 'assetId'> = {
 | 
						|
    duplicatesDetectedAt: date,
 | 
						|
    facesRecognizedAt: date,
 | 
						|
    metadataExtractedAt: date,
 | 
						|
    previewAt: date,
 | 
						|
    thumbnailAt: date,
 | 
						|
  };
 | 
						|
 | 
						|
  return {
 | 
						|
    ...defaults,
 | 
						|
    ...job,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const personInsert = (person: Partial<Insertable<Person>> & { ownerId: string }) => {
 | 
						|
  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,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertable<Sessions>> & { userId: string }) => {
 | 
						|
  const defaults: Insertable<Sessions> = {
 | 
						|
    id,
 | 
						|
    userId,
 | 
						|
    token: sha256(id),
 | 
						|
  };
 | 
						|
 | 
						|
  return {
 | 
						|
    ...defaults,
 | 
						|
    ...session,
 | 
						|
    id,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
 | 
						|
  const id = user.id || newUuid();
 | 
						|
 | 
						|
  const defaults: Insertable<UserTable> = {
 | 
						|
    email: `${id}@immich.cloud`,
 | 
						|
    name: `User ${id}`,
 | 
						|
    deletedAt: null,
 | 
						|
  };
 | 
						|
 | 
						|
  return { ...defaults, ...user, id };
 | 
						|
};
 | 
						|
 | 
						|
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();
 | 
						|
};
 | 
						|
 | 
						|
export const mediumFactory = {
 | 
						|
  assetInsert,
 | 
						|
  assetFaceInsert,
 | 
						|
  assetJobStatusInsert,
 | 
						|
  albumInsert,
 | 
						|
  faceInsert,
 | 
						|
  personInsert,
 | 
						|
  sessionInsert,
 | 
						|
  syncStream,
 | 
						|
  userInsert,
 | 
						|
};
 |