Merge branch 'main' into rknn-toolkit-lite2

This commit is contained in:
Yoni Yang 2025-01-24 12:20:29 +08:00 committed by GitHub
commit f9387d8478
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 723 additions and 581 deletions

View File

@ -68,10 +68,17 @@ jobs:
needs: build_mobile
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
token: ${{ steps.generate-token.outputs.token }}
- name: Download APK
uses: actions/download-artifact@v4

6
cli/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.40",
"version": "2.2.42",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.40",
"version": "2.2.42",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.40",
"version": "2.2.42",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@ -1,4 +1,12 @@
[
{
"label": "v1.125.1",
"url": "https://v1.125.1.archive.immich.app"
},
{
"label": "v1.125.0",
"url": "https://v1.125.0.archive.immich.app"
},
{
"label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app"

8
e2e/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.124.2",
"version": "1.125.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.40",
"version": "2.2.42",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.124.2",
"version": "1.125.1",
"description": "",
"main": "index.js",
"type": "module",

View File

@ -172,4 +172,31 @@ describe('/stacks', () => {
);
});
});
describe('GET /stacks/:id', () => {
it('should include exifInfo in stack assets', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const stack = await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
const { status, body } = await request(app)
.get(`/stacks/${stack.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: stack.id,
primaryAssetId: asset1.id,
assets: expect.arrayContaining([
expect.objectContaining({ id: asset1.id, exifInfo: expect.any(Object) }),
expect.objectContaining({ id: asset2.id, exifInfo: expect.any(Object) }),
]),
}),
);
});
});
});

View File

@ -822,6 +822,7 @@
"latest_version": "Latest Version",
"latitude": "Latitude",
"leave": "Leave",
"lens_model": "Lens model",
"let_others_respond": "Let others respond",
"level": "Level",
"library": "Library",
@ -1113,6 +1114,7 @@
"search_camera_model": "Search camera model...",
"search_city": "Search city...",
"search_country": "Search country...",
"search_for": "Search for",
"search_for_existing_person": "Search for existing person",
"search_no_people": "No people",
"search_no_people_named": "No people named \"{name}\"",

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.124.2"
version = "1.125.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 175,
"android.injected.version.name" => "1.124.2",
"android.injected.version.code" => 177,
"android.injected.version.name" => "1.125.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.124.2"
version_number: "1.125.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.124.2
- API version: 1.125.1
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.124.2+175
version: 1.125.1+177
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@ -7454,7 +7454,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.124.2",
"version": "1.125.1",
"contact": {}
},
"tags": [],

View File

@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@ -1,6 +1,6 @@
/**
* Immich
* 1.124.2
* 1.125.1
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.124.2",
"version": "1.125.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^11.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.124.2",
"version": "1.125.1",
"description": "",
"author": "",
"private": true,

View File

@ -13,7 +13,6 @@ import { entities } from 'src/entities';
import { ImmichWorker } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { AuthGuard } from 'src/middleware/auth.guard';
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
@ -22,7 +21,7 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
import { providers, repositories } from 'src/repositories';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { teardownTelemetry } from 'src/repositories/telemetry.repository';
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { services } from 'src/services';
import { CliService } from 'src/services/cli.service';
import { DatabaseService } from 'src/services/database.service';
@ -67,7 +66,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
logger: LoggingRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
private telemetryRepository: TelemetryRepository,
) {
logger.setAppName(this.worker);
}

View File

@ -3,8 +3,8 @@ import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { EmailTemplate } from 'src/interfaces/notification.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { NotificationService } from 'src/services/notification.service';
@ApiTags('Notifications')

View File

@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout';
import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
import { AlbumInviteEmailProps } from 'src/repositories/notification.repository';
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const AlbumInviteEmail = ({

View File

@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout';
import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
import { AlbumUpdateEmailProps } from 'src/repositories/notification.repository';
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const AlbumUpdateEmail = ({

View File

@ -1,7 +1,7 @@
import { Link, Row, Text } from '@react-email/components';
import * as React from 'react';
import ImmichLayout from 'src/emails/components/immich.layout';
import { TestEmailProps } from 'src/interfaces/notification.interface';
import { TestEmailProps } from 'src/repositories/notification.repository';
export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => (
<ImmichLayout preview="This is a test email from Immich.">

View File

@ -2,7 +2,7 @@ import { Link, Section, Text } from '@react-email/components';
import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout';
import { WelcomeEmailProps } from 'src/interfaces/notification.interface';
import { WelcomeEmailProps } from 'src/repositories/notification.repository';
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => {

View File

@ -1,18 +0,0 @@
import { Insertable, Selectable, Updateable } from 'kysely';
import { AlbumsSharedUsersUsers } from 'src/db';
export const IAlbumUserRepository = 'IAlbumUserRepository';
export type AlbumPermissionId = {
albumsId: string;
usersId: string;
};
export interface IAlbumUserRepository {
create(albumUser: Insertable<AlbumsSharedUsersUsers>): Promise<Selectable<AlbumsSharedUsersUsers>>;
update(
id: AlbumPermissionId,
albumPermission: Updateable<AlbumsSharedUsersUsers>,
): Promise<Selectable<AlbumsSharedUsersUsers>>;
delete(id: AlbumPermissionId): Promise<void>;
}

View File

@ -1,20 +0,0 @@
export const ICronRepository = 'ICronRepository';
type CronBase = {
name: string;
start?: boolean;
};
export type CronCreate = CronBase & {
expression: string;
onTick: () => void;
};
export type CronUpdate = CronBase & {
expression?: string;
};
export interface ICronRepository {
create(cron: CronCreate): void;
update(cron: CronUpdate): void;
}

View File

@ -1,5 +1,5 @@
import { ClassConstructor } from 'class-transformer';
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
import { EmailImageAttachment } from 'src/repositories/notification.repository';
export enum QueueName {
THUMBNAIL_GENERATION = 'thumbnailGeneration',

View File

@ -1,31 +0,0 @@
export const IMapRepository = 'IMapRepository';
export interface MapMarkerSearchOptions {
isArchived?: boolean;
isFavorite?: boolean;
fileCreatedBefore?: Date;
fileCreatedAfter?: Date;
}
export interface GeoPoint {
latitude: number;
longitude: number;
}
export interface ReverseGeocodeResult {
country: string | null;
state: string | null;
city: string | null;
}
export interface MapMarker extends ReverseGeocodeResult {
id: string;
lat: number;
lon: number;
}
export interface IMapRepository {
init(): Promise<void>;
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
}

View File

@ -1,71 +0,0 @@
import { BinaryField, Tags } from 'exiftool-vendored';
export const IMetadataRepository = 'IMetadataRepository';
export interface ExifDuration {
Value: number;
Scale?: number;
}
type StringOrNumber = string | number;
type TagsWithWrongTypes =
| 'FocalLength'
| 'Duration'
| 'Description'
| 'ImageDescription'
| 'RegionInfo'
| 'TagsList'
| 'Keywords'
| 'HierarchicalSubject'
| 'ISO';
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
ContentIdentifier?: string;
MotionPhoto?: number;
MotionPhotoVersion?: number;
MotionPhotoPresentationTimestampUs?: number;
MediaGroupUUID?: string;
ImagePixelDepth?: string;
FocalLength?: number;
Duration?: number | string | ExifDuration;
EmbeddedVideoType?: string;
EmbeddedVideoFile?: BinaryField;
MotionPhotoVideo?: BinaryField;
TagsList?: StringOrNumber[];
HierarchicalSubject?: StringOrNumber[];
Keywords?: StringOrNumber | StringOrNumber[];
ISO?: number | number[];
// Type is wrong, can also be number.
Description?: StringOrNumber;
ImageDescription?: StringOrNumber;
// Extended properties for image regions, such as faces
RegionInfo?: {
AppliedToDimensions: {
W: number;
H: number;
Unit: string;
};
RegionList: {
Area: {
// (X,Y) // center of the rectangle
X: number;
Y: number;
W: number;
H: number;
Unit: string;
};
Rotation?: number;
Type?: string;
Name?: string;
}[];
};
}
export interface IMetadataRepository {
teardown(): Promise<void>;
readTags(path: string): Promise<ImmichTags>;
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
}

View File

@ -1,101 +0,0 @@
export const INotificationRepository = 'INotificationRepository';
export type EmailImageAttachment = {
filename: string;
path: string;
cid: string;
};
export type SendEmailOptions = {
from: string;
to: string;
replyTo?: string;
subject: string;
html: string;
text: string;
imageAttachments?: EmailImageAttachment[];
smtp: SmtpOptions;
};
export type SmtpOptions = {
host: string;
port?: number;
username?: string;
password?: string;
ignoreCert?: boolean;
};
export enum EmailTemplate {
TEST_EMAIL = 'test',
// AUTH
WELCOME = 'welcome',
RESET_PASSWORD = 'reset-password',
// ALBUM
ALBUM_INVITE = 'album-invite',
ALBUM_UPDATE = 'album-update',
}
interface BaseEmailProps {
baseUrl: string;
customTemplate?: string;
}
export interface TestEmailProps extends BaseEmailProps {
displayName: string;
}
export interface WelcomeEmailProps extends BaseEmailProps {
displayName: string;
username: string;
password?: string;
}
export interface AlbumInviteEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
senderName: string;
recipientName: string;
cid?: string;
}
export interface AlbumUpdateEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
recipientName: string;
cid?: string;
}
export type EmailRenderRequest =
| {
template: EmailTemplate.TEST_EMAIL;
data: TestEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.WELCOME;
data: WelcomeEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_INVITE;
data: AlbumInviteEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_UPDATE;
data: AlbumUpdateEmailProps;
customTemplate: string;
};
export type SendEmailResponse = {
messageId: string;
response: any;
};
export interface INotificationRepository {
renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>;
sendEmail(options: SendEmailOptions): Promise<SendEmailResponse>;
verifySmtp(options: SmtpOptions): Promise<true>;
}

View File

@ -1,22 +0,0 @@
import { UserinfoResponse } from 'openid-client';
export const IOAuthRepository = 'IOAuthRepository';
export type OAuthConfig = {
clientId: string;
clientSecret: string;
issuerUrl: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
profileSigningAlgorithm: string;
scope: string;
signingAlgorithm: string;
};
export type OAuthProfile = UserinfoResponse;
export interface IOAuthRepository {
init(): void;
authorize(config: OAuthConfig, redirectUrl: string): Promise<string>;
getLogoutEndpoint(config: OAuthConfig): Promise<string | undefined>;
getProfile(config: OAuthConfig, url: string, redirectUrl: string): Promise<OAuthProfile>;
}

View File

@ -1,24 +0,0 @@
export interface GitHubRelease {
id: number;
url: string;
tag_name: string;
name: string;
created_at: string;
published_at: string;
body: string;
}
export interface ServerBuildVersions {
nodejs: string;
ffmpeg: string;
libvips: string;
exiftool: string;
imagemagick: string;
}
export const IServerInfoRepository = 'IServerInfoRepository';
export interface IServerInfoRepository {
getGitHubRelease(): Promise<GitHubRelease>;
getBuildVersions(): Promise<ServerBuildVersions>;
}

View File

@ -1,23 +0,0 @@
import { MetricOptions } from '@opentelemetry/api';
import { ClassConstructor } from 'class-transformer';
export const ITelemetryRepository = 'ITelemetryRepository';
export interface MetricGroupOptions {
enabled: boolean;
}
export interface IMetricGroupRepository {
addToCounter(name: string, value: number, options?: MetricOptions): void;
addToGauge(name: string, value: number, options?: MetricOptions): void;
addToHistogram(name: string, value: number, options?: MetricOptions): void;
configure(options: MetricGroupOptions): this;
}
export interface ITelemetryRepository {
setup(options: { repositories: ClassConstructor<unknown>[] }): void;
api: IMetricGroupRepository;
host: IMetricGroupRepository;
jobs: IMetricGroupRepository;
repo: IMetricGroupRepository;
}

View File

@ -1,8 +0,0 @@
export const ITrashRepository = 'ITrashRepository';
export interface ITrashRepository {
empty(userId: string): Promise<number>;
restore(userId: string): Promise<number>;
restoreAll(assetIds: string[]): Promise<number>;
getDeletedIds(): AsyncIterableIterator<{ id: string }>;
}

View File

@ -1,9 +0,0 @@
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
export const IVersionHistoryRepository = 'IVersionHistoryRepository';
export interface IVersionHistoryRepository {
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>;
getAll(): Promise<VersionHistoryEntity[]>;
getLatest(): Promise<VersionHistoryEntity | undefined>;
}

View File

@ -0,0 +1,102 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUpdatedAtTriggers1737672307560 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create function updated_at()
returns trigger as $$
begin
new."updatedAt" = now();
return new;
end;
$$ language 'plpgsql'`);
await queryRunner.query(`
create trigger activity_updated_at
before update on activity
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger albums_updated_at
before update on albums
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger api_keys_updated_at
before update on api_keys
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger asset_files_updated_at
before update on asset_files
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger assets_updated_at
before update on assets
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger libraries_updated_at
before update on libraries
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger memories_updated_at
before update on memories
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger partners_updated_at
before update on partners
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger person_updated_at
before update on person
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger sessions_updated_at
before update on sessions
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger tags_updated_at
before update on tags
for each row execute procedure updated_at()
`);
await queryRunner.query(`
create trigger users_updated_at
before update on users
for each row execute procedure updated_at()
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop trigger activity_updated_at on activity`);
await queryRunner.query(`drop trigger albums_updated_at on albums`);
await queryRunner.query(`drop trigger api_keys_updated_at on api_keys`);
await queryRunner.query(`drop trigger asset_files_updated_at on asset_files`);
await queryRunner.query(`drop trigger assets_updated_at on assets`);
await queryRunner.query(`drop trigger libraries_updated_at on libraries`);
await queryRunner.query(`drop trigger memories_updated_at on memories`);
await queryRunner.query(`drop trigger partners_updated_at on partners`);
await queryRunner.query(`drop trigger person_updated_at on person`);
await queryRunner.query(`drop trigger sessions_updated_at on sessions`);
await queryRunner.query(`drop trigger tags_updated_at on tags`);
await queryRunner.query(`drop trigger users_updated_at on users`);
await queryRunner.query(`drop function updated_at_trigger`);
}
}

View File

@ -9,9 +9,18 @@ select
from
(
select
*
"assets".*,
to_json("exifInfo") as "exifInfo"
from
"assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where
"assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id"
@ -31,7 +40,7 @@ select
from
(
select
*,
"assets".*,
(
select
coalesce(json_agg(agg), '[]')
@ -45,9 +54,18 @@ select
where
"tag_asset"."assetsId" = "assets"."id"
) as agg
) as "tags"
) as "tags",
to_json("exifInfo") as "exifInfo"
from
"assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where
"assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id"
@ -67,7 +85,7 @@ select
from
(
select
*,
"assets".*,
(
select
coalesce(json_agg(agg), '[]')
@ -81,9 +99,18 @@ select
where
"tag_asset"."assetsId" = "assets"."id"
) as agg
) as "tags"
) as "tags",
to_json("exifInfo") as "exifInfo"
from
"assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where
"assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id"

View File

@ -4,10 +4,14 @@ import { InjectKysely } from 'nestjs-kysely';
import { AlbumsSharedUsersUsers, DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface';
export type AlbumPermissionId = {
albumsId: string;
usersId: string;
};
@Injectable()
export class AlbumUserRepository implements IAlbumUserRepository {
export class AlbumUserRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] })
@ -16,10 +20,7 @@ export class AlbumUserRepository implements IAlbumUserRepository {
}
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] })
update(
{ usersId, albumsId }: AlbumPermissionId,
dto: Updateable<AlbumsSharedUsersUsers>,
): Promise<Selectable<AlbumsSharedUsersUsers>> {
update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable<AlbumsSharedUsersUsers>) {
return this.db
.updateTable('albums_shared_users_users')
.set(dto)

View File

@ -43,8 +43,8 @@ import {
WithProperty,
WithoutProperty,
} from 'src/interfaces/asset.interface';
import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface';
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface';
import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository';
import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database';
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
@ -605,10 +605,10 @@ export class AssetRepository implements IAssetRepository {
.where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])),
)
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(!!options.isArchived, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(!!options.isFavorite, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(!!options.isDuplicate, (qb) =>
.$if(options.isDuplicate !== undefined, (qb) =>
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
)
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
@ -622,7 +622,7 @@ export class AssetRepository implements IAssetRepository {
*/
.select((eb) => eb.fn.countAll().as('count'))
.groupBy('timeBucket')
.orderBy('timeBucket', 'desc')
.orderBy('timeBucket', options.order ?? 'desc')
.execute() as any as Promise<TimeBucketItem[]>
);
}
@ -666,7 +666,7 @@ export class AssetRepository implements IAssetRepository {
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.where('assets.isVisible', '=', true)
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
.orderBy('assets.localDateTime', 'desc')
.orderBy('assets.localDateTime', options.order ?? 'desc')
.execute() as any as Promise<AssetEntity[]>;
}

View File

@ -1,11 +1,24 @@
import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob, CronTime } from 'cron';
import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
type CronBase = {
name: string;
start?: boolean;
};
export type CronCreate = CronBase & {
expression: string;
onTick: () => void;
};
export type CronUpdate = CronBase & {
expression?: string;
};
@Injectable()
export class CronRepository implements ICronRepository {
export class CronRepository {
constructor(
private schedulerRegistry: SchedulerRegistry,
private logger: LoggingRepository,

View File

@ -1,33 +1,23 @@
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface';
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@ -71,44 +61,44 @@ import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [
AccessRepository,
ActivityRepository,
AlbumUserRepository,
AuditRepository,
ApiKeyRepository,
ConfigRepository,
CronRepository,
LoggingRepository,
MapRepository,
MediaRepository,
MemoryRepository,
MetadataRepository,
NotificationRepository,
OAuthRepository,
ServerInfoRepository,
TelemetryRepository,
TrashRepository,
ViewRepository,
VersionHistoryRepository,
];
export const providers = [
{ provide: IAlbumRepository, useClass: AlbumRepository },
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: ICronRepository, useClass: CronRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
{ provide: IEventRepository, useClass: EventRepository },
{ provide: IJobRepository, useClass: JobRepository },
{ provide: ILibraryRepository, useClass: LibraryRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMapRepository, useClass: MapRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository },
{ provide: INotificationRepository, useClass: NotificationRepository },
{ provide: IOAuthRepository, useClass: OAuthRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: IProcessRepository, useClass: ProcessRepository },
{ provide: ISearchRepository, useClass: SearchRepository },
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository },
{ provide: ITelemetryRepository, useClass: TelemetryRepository },
{ provide: ITrashRepository, useClass: TrashRepository },
{ provide: IUserRepository, useClass: UserRepository },
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
];

View File

@ -11,24 +11,41 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { AssetEntity, withExif } from 'src/entities/asset.entity';
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
import { LogLevel, SystemMetadataKey } from 'src/enum';
import {
GeoPoint,
IMapRepository,
MapMarker,
MapMarkerSearchOptions,
ReverseGeocodeResult,
} from 'src/interfaces/map.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
export interface MapMarkerSearchOptions {
isArchived?: boolean;
isFavorite?: boolean;
fileCreatedBefore?: Date;
fileCreatedAfter?: Date;
}
export interface GeoPoint {
latitude: number;
longitude: number;
}
export interface ReverseGeocodeResult {
country: string | null;
state: string | null;
city: string | null;
}
export interface MapMarker extends ReverseGeocodeResult {
id: string;
lat: number;
lon: number;
}
interface MapDB extends DB {
geodata_places_tmp: GeodataPlaces;
naturalearth_countries_tmp: NaturalearthCountries;
}
@Injectable()
export class MapRepository implements IMapRepository {
export class MapRepository {
constructor(
private configRepository: ConfigRepository,
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,

View File

@ -1,11 +1,72 @@
import { Injectable } from '@nestjs/common';
import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import geotz from 'geo-tz';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
interface ExifDuration {
Value: number;
Scale?: number;
}
type StringOrNumber = string | number;
type TagsWithWrongTypes =
| 'FocalLength'
| 'Duration'
| 'Description'
| 'ImageDescription'
| 'RegionInfo'
| 'TagsList'
| 'Keywords'
| 'HierarchicalSubject'
| 'ISO';
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
ContentIdentifier?: string;
MotionPhoto?: number;
MotionPhotoVersion?: number;
MotionPhotoPresentationTimestampUs?: number;
MediaGroupUUID?: string;
ImagePixelDepth?: string;
FocalLength?: number;
Duration?: number | string | ExifDuration;
EmbeddedVideoType?: string;
EmbeddedVideoFile?: BinaryField;
MotionPhotoVideo?: BinaryField;
TagsList?: StringOrNumber[];
HierarchicalSubject?: StringOrNumber[];
Keywords?: StringOrNumber | StringOrNumber[];
ISO?: number | number[];
// Type is wrong, can also be number.
Description?: StringOrNumber;
ImageDescription?: StringOrNumber;
// Extended properties for image regions, such as faces
RegionInfo?: {
AppliedToDimensions: {
W: number;
H: number;
Unit: string;
};
RegionList: {
Area: {
// (X,Y) // center of the rectangle
X: number;
Y: number;
W: number;
H: number;
Unit: string;
};
Rotation?: number;
Type?: string;
Name?: string;
}[];
};
}
@Injectable()
export class MetadataRepository implements IMetadataRepository {
export class MetadataRepository {
private exiftool = new ExifTool({
defaultVideosToUTC: true,
backfillTimezones: true,

View File

@ -1,6 +1,5 @@
import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
import { ILoggingRepository } from 'src/types';
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';

View File

@ -6,18 +6,104 @@ import { AlbumInviteEmail } from 'src/emails/album-invite.email';
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
import { TestEmail } from 'src/emails/test.email';
import { WelcomeEmail } from 'src/emails/welcome.email';
import {
EmailRenderRequest,
EmailTemplate,
INotificationRepository,
SendEmailOptions,
SendEmailResponse,
SmtpOptions,
} from 'src/interfaces/notification.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
export type EmailImageAttachment = {
filename: string;
path: string;
cid: string;
};
export type SendEmailOptions = {
from: string;
to: string;
replyTo?: string;
subject: string;
html: string;
text: string;
imageAttachments?: EmailImageAttachment[];
smtp: SmtpOptions;
};
export type SmtpOptions = {
host: string;
port?: number;
username?: string;
password?: string;
ignoreCert?: boolean;
};
export enum EmailTemplate {
TEST_EMAIL = 'test',
// AUTH
WELCOME = 'welcome',
RESET_PASSWORD = 'reset-password',
// ALBUM
ALBUM_INVITE = 'album-invite',
ALBUM_UPDATE = 'album-update',
}
interface BaseEmailProps {
baseUrl: string;
customTemplate?: string;
}
export interface TestEmailProps extends BaseEmailProps {
displayName: string;
}
export interface WelcomeEmailProps extends BaseEmailProps {
displayName: string;
username: string;
password?: string;
}
export interface AlbumInviteEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
senderName: string;
recipientName: string;
cid?: string;
}
export interface AlbumUpdateEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
recipientName: string;
cid?: string;
}
export type EmailRenderRequest =
| {
template: EmailTemplate.TEST_EMAIL;
data: TestEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.WELCOME;
data: WelcomeEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_INVITE;
data: AlbumInviteEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_UPDATE;
data: AlbumUpdateEmailProps;
customTemplate: string;
};
export type SendEmailResponse = {
messageId: string;
response: any;
};
@Injectable()
export class NotificationRepository implements INotificationRepository {
export class NotificationRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(NotificationRepository.name);
}

View File

@ -1,10 +1,21 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { custom, generators, Issuer } from 'openid-client';
import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface';
import { custom, generators, Issuer, UserinfoResponse } from 'openid-client';
import { LoggingRepository } from 'src/repositories/logging.repository';
export type OAuthConfig = {
clientId: string;
clientSecret: string;
issuerUrl: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
profileSigningAlgorithm: string;
scope: string;
signingAlgorithm: string;
};
export type OAuthProfile = UserinfoResponse;
@Injectable()
export class OAuthRepository implements IOAuthRepository {
export class OAuthRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(OAuthRepository.name);
}

View File

@ -133,10 +133,6 @@ export class PersonRepository implements IPersonRepository {
)
.where('person.ownerId', '=', userId)
.orderBy('person.isHidden', 'asc')
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
.orderBy('person.createdAt')
.having((eb) =>
eb.or([
eb('person.name', '!=', ''),
@ -161,6 +157,13 @@ export class PersonRepository implements IPersonRepository {
),
),
)
.$if(!options?.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
.orderBy('person.createdAt'),
)
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
.offset(pagination.skip ?? 0)
.limit(pagination.take + 1)

View File

@ -4,10 +4,27 @@ import { exec as execCallback } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
export interface GitHubRelease {
id: number;
url: string;
tag_name: string;
name: string;
created_at: string;
published_at: string;
body: string;
}
export interface ServerBuildVersions {
nodejs: string;
ffmpeg: string;
libvips: string;
exiftool: string;
imagemagick: string;
}
const exec = promisify(execCallback);
const maybeFirstLine = async (command: string): Promise<string> => {
try {
@ -34,7 +51,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
};
@Injectable()
export class ServerInfoRepository implements IServerInfoRepository {
export class ServerInfoRepository {
constructor(
private configRepository: ConfigRepository,
private logger: LoggingRepository,

View File

@ -12,7 +12,11 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
return jsonArrayFrom(
eb
.selectFrom('assets')
.selectAll()
.selectAll('assets')
.innerJoinLateral(
(eb) => eb.selectFrom('exif').selectAll('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
(join) => join.onTrue(),
)
.$if(withTags, (eb) =>
eb.select((eb) =>
jsonArrayFrom(
@ -24,6 +28,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
).as('tags'),
),
)
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
.where('assets.deletedAt', 'is', null)
.whereRef('assets.stackId', '=', 'asset_stack.id'),
).as('assets');

View File

@ -15,11 +15,12 @@ import { MetricService } from 'nestjs-otel';
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
import { serverVersion } from 'src/constants';
import { ImmichTelemetry, MetadataKey } from 'src/enum';
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
class MetricGroupRepository implements IMetricGroupRepository {
type MetricGroupOptions = { enabled: boolean };
export class MetricGroupRepository {
private enabled = false;
constructor(private metricService: MetricService) {}
@ -86,7 +87,7 @@ export const teardownTelemetry = async () => {
};
@Injectable()
export class TelemetryRepository implements ITelemetryRepository {
export class TelemetryRepository {
api: MetricGroupRepository;
host: MetricGroupRepository;
jobs: MetricGroupRepository;

View File

@ -3,9 +3,8 @@ import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetStatus } from 'src/enum';
import { ITrashRepository } from 'src/interfaces/trash.interface';
export class TrashRepository implements ITrashRepository {
export class TrashRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
getDeletedIds(): AsyncIterableIterator<{ id: string }> {

View File

@ -3,25 +3,23 @@ import { Insertable, Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB, VersionHistory } from 'src/db';
import { GenerateSql } from 'src/decorators';
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
@Injectable()
export class VersionHistoryRepository implements IVersionHistoryRepository {
export class VersionHistoryRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql()
getAll(): Promise<VersionHistoryEntity[]> {
getAll() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute();
}
@GenerateSql()
getLatest(): Promise<VersionHistoryEntity | undefined> {
getLatest() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
}
@GenerateSql({ params: [{ version: 'v1.123.0' }] })
create(version: Insertable<VersionHistory>): Promise<VersionHistoryEntity> {
create(version: Insertable<VersionHistory>) {
return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow();
}
}

View File

@ -2,11 +2,11 @@ import { BadRequestException } from '@nestjs/common';
import _ from 'lodash';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { AlbumUserRole } from 'src/enum';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AlbumService } from 'src/services/album.service';
import { IAlbumUserRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';

View File

@ -5,13 +5,12 @@ import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service';
import { IApiKeyRepository } from 'src/types';
import { IApiKeyRepository, IOAuthRepository } from 'src/types';
import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';

View File

@ -19,7 +19,7 @@ import {
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
import { OAuthProfile } from 'src/interfaces/oauth.interface';
import { OAuthProfile } from 'src/repositories/oauth.repository';
import { BaseService } from 'src/services/base.service';
import { AuthApiKey } from 'src/types';
import { isGranted } from 'src/utils/access';

View File

@ -2,14 +2,13 @@ import { PassThrough } from 'node:stream';
import { defaults, SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { ImmichWorker, StorageFolder } from 'src/enum';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BackupService } from 'src/services/backup.service';
import { IConfigRepository } from 'src/types';
import { IConfigRepository, ICronRepository } from 'src/types';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { mockSpawn, newTestService } from 'test/utils';
import { describe, Mocked } from 'vitest';

View File

@ -6,44 +6,44 @@ import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { Users } from 'src/db';
import { UserEntity } from 'src/entities/user.entity';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface';
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { CronRepository } from 'src/repositories/cron.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config';
@ -57,10 +57,10 @@ export class BaseService {
protected activityRepository: ActivityRepository,
protected auditRepository: AuditRepository,
@Inject(IAlbumRepository) protected albumRepository: IAlbumRepository,
@Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository,
protected albumUserRepository: AlbumUserRepository,
@Inject(IAssetRepository) protected assetRepository: IAssetRepository,
protected configRepository: ConfigRepository,
@Inject(ICronRepository) protected cronRepository: ICronRepository,
protected cronRepository: CronRepository,
@Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) protected eventRepository: IEventRepository,
@ -68,28 +68,28 @@ export class BaseService {
protected keyRepository: ApiKeyRepository,
@Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository,
@Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository,
@Inject(IMapRepository) protected mapRepository: IMapRepository,
protected mapRepository: MapRepository,
protected mediaRepository: MediaRepository,
protected memoryRepository: MemoryRepository,
@Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository,
protected metadataRepository: MetadataRepository,
@Inject(IMoveRepository) protected moveRepository: IMoveRepository,
@Inject(INotificationRepository) protected notificationRepository: INotificationRepository,
@Inject(IOAuthRepository) protected oauthRepository: IOAuthRepository,
protected notificationRepository: NotificationRepository,
protected oauthRepository: OAuthRepository,
@Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository,
@Inject(IPersonRepository) protected personRepository: IPersonRepository,
@Inject(IProcessRepository) protected processRepository: IProcessRepository,
@Inject(ISearchRepository) protected searchRepository: ISearchRepository,
@Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository,
protected serverInfoRepository: ServerInfoRepository,
@Inject(ISessionRepository) protected sessionRepository: ISessionRepository,
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
@Inject(IStackRepository) protected stackRepository: IStackRepository,
@Inject(IStorageRepository) protected storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ITagRepository) protected tagRepository: ITagRepository,
@Inject(ITelemetryRepository) protected telemetryRepository: ITelemetryRepository,
@Inject(ITrashRepository) protected trashRepository: ITrashRepository,
protected telemetryRepository: TelemetryRepository,
protected trashRepository: TrashRepository,
@Inject(IUserRepository) protected userRepository: IUserRepository,
@Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository,
protected versionRepository: VersionHistoryRepository,
protected viewRepository: ViewRepository,
) {
this.logger.setContext(this.constructor.name);

View File

@ -3,10 +3,10 @@ import { defaults, SystemConfig } from 'src/config';
import { ImmichWorker } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { JobService } from 'src/services/job.service';
import { IConfigRepository, ILoggingRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { ITelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
@ -16,7 +16,7 @@ describe(JobService.name, () => {
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggingRepository>;
let telemetryMock: Mocked<ITelemetryRepository>;
let telemetryMock: ITelemetryRepositoryMock;
beforeEach(() => {
({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {}));

View File

@ -5,7 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AssetType, ImmichWorker } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import {
IJobRepository,
@ -18,7 +17,7 @@ import {
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { LibraryService } from 'src/services/library.service';
import { IConfigRepository } from 'src/types';
import { IConfigRepository, ICronRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { libraryStub } from 'test/fixtures/library.stub';

View File

@ -1,7 +1,7 @@
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { MapService } from 'src/services/map.service';
import { IMapRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';

View File

@ -10,15 +10,14 @@ import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interfac
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import { IConfigRepository, IMediaRepository } from 'src/types';
import { IConfigRepository, IMapRepository, IMediaRepository, IMetadataRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { probeStub } from 'test/fixtures/media.stub';

View File

@ -18,8 +18,8 @@ import { WithoutProperty } from 'src/interfaces/asset.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { ReverseGeocodeResult } from 'src/interfaces/map.interface';
import { ImmichTags } from 'src/interfaces/metadata.interface';
import { ReverseGeocodeResult } from 'src/repositories/map.repository';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { BaseService } from 'src/services/base.service';
import { isFaceImportEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';

View File

@ -8,10 +8,11 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface';
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { NotificationService } from 'src/services/notification.service';
import { INotificationRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';

View File

@ -12,7 +12,7 @@ import {
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface';
import { EmailImageAttachment, EmailTemplate } from 'src/repositories/notification.repository';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file';

View File

@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { TrashService } from 'src/services/trash.service';
import { ITrashRepository } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newTestService } from 'test/utils';

View File

@ -4,11 +4,9 @@ import { serverVersion } from 'src/constants';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { VersionService } from 'src/services/version.service';
import { IConfigRepository, ILoggingRepository } from 'src/types';
import { IConfigRepository, ILoggingRepository, IServerInfoRepository, IVersionHistoryRepository } from 'src/types';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@ -2,12 +2,22 @@ import { UserEntity } from 'src/entities/user.entity';
import { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } 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 { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { CronRepository } from 'src/repositories/cron.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
@ -23,9 +33,11 @@ export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
export type IActivityRepository = RepositoryInterface<ActivityRepository>;
export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
export type IAlbumUserRepository = RepositoryInterface<AlbumUserRepository>;
export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>;
export type IAuditRepository = RepositoryInterface<AuditRepository>;
export type IConfigRepository = RepositoryInterface<ConfigRepository>;
export type ICronRepository = RepositoryInterface<CronRepository>;
export type ILoggingRepository = Pick<
LoggingRepository,
| 'verbose'
@ -39,9 +51,18 @@ export type ILoggingRepository = Pick<
| 'setContext'
| 'setAppName'
>;
export type IMapRepository = RepositoryInterface<MapRepository>;
export type IMediaRepository = RepositoryInterface<MediaRepository>;
export type IMemoryRepository = RepositoryInterface<MemoryRepository>;
export type IMetadataRepository = RepositoryInterface<MetadataRepository>;
export type IMetricGroupRepository = RepositoryInterface<MetricGroupRepository>;
export type INotificationRepository = RepositoryInterface<NotificationRepository>;
export type IOAuthRepository = RepositoryInterface<OAuthRepository>;
export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>;
export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
export type ITrashRepository = RepositoryInterface<TrashRepository>;
export type IViewRepository = RepositoryInterface<ViewRepository>;
export type IVersionHistoryRepository = RepositoryInterface<VersionHistoryRepository>;
export type ActivityItem =
| Awaited<ReturnType<IActivityRepository['create']>>

View File

@ -1,4 +1,4 @@
import { ImmichTags } from 'src/interfaces/metadata.interface';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { personStub } from 'test/fixtures/person.stub';
export const metadataStub = {

View File

@ -1,4 +1,4 @@
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumUserRepository } from 'src/types';
import { Mocked } from 'vitest';
export const newAlbumUserRepositoryMock = (): Mocked<IAlbumUserRepository> => {

View File

@ -1,4 +1,4 @@
import { ICronRepository } from 'src/interfaces/cron.interface';
import { ICronRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newCronRepositoryMock = (): Mocked<ICronRepository> => {

View File

@ -1,4 +1,4 @@
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMapRepository } from 'src/types';
import { Mocked } from 'vitest';
export const newMapRepositoryMock = (): Mocked<IMapRepository> => {

View File

@ -1,4 +1,4 @@
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMetadataRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => {

View File

@ -1,4 +1,4 @@
import { INotificationRepository } from 'src/interfaces/notification.interface';
import { INotificationRepository } from 'src/types';
import { Mocked } from 'vitest';
export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => {

View File

@ -1,4 +1,4 @@
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { IOAuthRepository } from 'src/types';
import { Mocked } from 'vitest';
export const newOAuthRepositoryMock = (): Mocked<IOAuthRepository> => {

View File

@ -1,4 +1,4 @@
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { IServerInfoRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newServerInfoRepositoryMock = (): Mocked<IServerInfoRepository> => {

View File

@ -1,4 +1,4 @@
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { ITelemetryRepository, RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
const newMetricGroupMock = () => {
@ -10,7 +10,11 @@ const newMetricGroupMock = () => {
};
};
export const newTelemetryRepositoryMock = (): Mocked<ITelemetryRepository> => {
export type ITelemetryRepositoryMock = {
[K in keyof ITelemetryRepository]: Mocked<RepositoryInterface<ITelemetryRepository[K]>>;
};
export const newTelemetryRepositoryMock = (): ITelemetryRepositoryMock => {
return {
setup: vitest.fn(),
api: newMetricGroupMock(),

View File

@ -1,4 +1,4 @@
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { ITrashRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newTrashRepositoryMock = (): Mocked<ITrashRepository> => {

View File

@ -1,4 +1,4 @@
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { IVersionHistoryRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {

View File

@ -2,24 +2,42 @@ import { ChildProcessWithoutNullStreams } from 'node:child_process';
import { Writable } from 'node:stream';
import { PNG } from 'pngjs';
import { ImmichWorker } from 'src/enum';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
import { CronRepository } from 'src/repositories/cron.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
import { BaseService } from 'src/services/base.service';
import {
IAccessRepository,
IActivityRepository,
IAlbumUserRepository,
IApiKeyRepository,
IAuditRepository,
ICronRepository,
ILoggingRepository,
IMapRepository,
IMediaRepository,
IMemoryRepository,
IMetadataRepository,
INotificationRepository,
IOAuthRepository,
IServerInfoRepository,
ITrashRepository,
IVersionHistoryRepository,
IViewRepository,
} from 'src/types';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@ -66,7 +84,7 @@ import { Mocked, vitest } from 'vitest';
type Overrides = {
worker?: ImmichWorker;
metadataRepository?: IMetadataRepository;
metadataRepository?: MetadataRepository;
};
type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
type Constructor<Type, Args extends Array<any>> = {
@ -125,10 +143,10 @@ export const newTestService = <T extends BaseService>(
activityMock as IActivityRepository as ActivityRepository,
auditMock as IAuditRepository as AuditRepository,
albumMock,
albumUserMock,
albumUserMock as IAlbumUserRepository as AlbumUserRepository,
assetMock,
configMock,
cronMock,
cronMock as ICronRepository as CronRepository,
cryptoMock,
databaseMock,
eventMock,
@ -136,28 +154,28 @@ export const newTestService = <T extends BaseService>(
keyMock as IApiKeyRepository as ApiKeyRepository,
libraryMock,
machineLearningMock,
mapMock,
mapMock as IMapRepository as MapRepository,
mediaMock as IMediaRepository as MediaRepository,
memoryMock as IMemoryRepository as MemoryRepository,
metadataMock,
metadataMock as IMetadataRepository as MetadataRepository,
moveMock,
notificationMock,
oauthMock,
notificationMock as INotificationRepository as NotificationRepository,
oauthMock as IOAuthRepository as OAuthRepository,
partnerMock,
personMock,
processMock,
searchMock,
serverInfoMock,
serverInfoMock as IServerInfoRepository as ServerInfoRepository,
sessionMock,
sharedLinkMock,
stackMock,
storageMock,
systemMock,
tagMock,
telemetryMock,
trashMock,
telemetryMock as unknown as TelemetryRepository,
trashMock as ITrashRepository as TrashRepository,
userMock,
versionHistoryMock,
versionHistoryMock as IVersionHistoryRepository as VersionHistoryRepository,
viewMock as IViewRepository as ViewRepository,
);

8
web/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.124.2",
"version": "1.125.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@ -75,13 +75,13 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.10.7",
"@types/node": "^22.10.9",
"typescript": "^5.3.3"
}
},

View File

@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.124.2",
"version": "1.125.1",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",

View File

@ -45,6 +45,7 @@
import UserAvatar from '../shared-components/user-avatar.svelte';
import AlbumListItemDetails from './album-list-item-details.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
interface Props {
asset: AssetResponseDto;
@ -410,7 +411,36 @@
<div><Icon path={mdiCameraIris} size="24" /></div>
<div>
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
{#if asset.exifInfo?.make || asset.exifInfo?.model}
<p>
<a
href="{AppRoute.SEARCH}?{getMetadataSearchQuery({
...(asset.exifInfo?.make ? { make: asset.exifInfo.make } : {}),
...(asset.exifInfo?.model ? { model: asset.exifInfo.model } : {}),
})}"
title="{$t('search_for')} {asset.exifInfo.make || ''} {asset.exifInfo.model || ''}"
class="hover:dark:text-immich-dark-primary hover:text-immich-primary"
>
{asset.exifInfo.make || ''}
{asset.exifInfo.model || ''}
</a>
</p>
{/if}
{#if asset.exifInfo?.lensModel}
<div class="flex gap-2 text-sm">
<p>
<a
href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}"
title="{$t('search_for')} {asset.exifInfo.lensModel}"
class="hover:dark:text-immich-dark-primary hover:text-immich-primary line-clamp-1"
>
{asset.exifInfo.lensModel}
</a>
</p>
</div>
{/if}
<div class="flex gap-2 text-sm">
{#if asset.exifInfo?.fNumber}
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>

View File

@ -10,7 +10,12 @@
let { title, children }: Props = $props();
</script>
<section class="min-w-screen flex min-h-screen items-center justify-center">
<section class="min-w-screen flex min-h-dvh items-center justify-center relative">
<div class="absolute -z-10 w-[99%] h-[99%] flex place-items-center place-content-center">
<img src="/immich-logo-no-bg.svg" class="w-[750px] mb-2 antialiased -z-10" alt="Immich logo" />
<div class="w-full h-[99%] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/80 dark:bg-immich-dark-bg/20"></div>
</div>
<Card color="secondary" class="w-full max-w-lg border m-2">
<CardHeader class="mt-6">
<VStack>
@ -18,7 +23,8 @@
<Heading size="large" class="font-semibold" color="primary">{title}</Heading>
</VStack>
</CardHeader>
<CardBody>
<CardBody class="p-8">
{@render children?.()}
</CardBody>
</Card>

View File

@ -192,6 +192,7 @@
state: $t('state'),
make: $t('camera_brand'),
model: $t('camera_model'),
lensModel: $t('lens_model'),
personIds: $t('people'),
originalFileName: $t('file_name'),
};

View File

@ -34,17 +34,14 @@
</script>
<AuthPageLayout title={data.meta.title}>
<div class="m-4">
<Alert color="primary" size="small">
<form onsubmit={onSubmit} method="post" class="flex flex-col gap-4">
<Alert color="primary" size="small" class="mb-2">
<Stack gap={4}>
<Text>{$t('hi_user', { values: { name: $user.name, email: $user.email } })}</Text>
<Text>{$t('change_password_description')}</Text>
</Stack>
</Alert>
</div>
<form onsubmit={onSubmit} method="post" class="mx-4 mt-6">
<Stack gap={4} class="mt-4">
<Field label={$t('new_password')} required>
<PasswordInput bind:value={password} autocomplete="new-password" />
</Field>
@ -54,9 +51,8 @@
<HelperText color="danger">{errorMessage}</HelperText>
</Field>
<div class="my-5 flex w-full">
<Button type="submit" size="large" shape="round" fullWidth disabled={!valid}>{$t('to_change_password')}</Button>
</div>
</Stack>
<Button class="mt-2" type="submit" size="large" shape="round" fullWidth disabled={!valid}
>{$t('to_change_password')}</Button
>
</form>
</AuthPageLayout>

View File

@ -6,7 +6,7 @@
import { oauth } from '$lib/utils';
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
import { login } from '@immich/sdk';
import { Alert, Button, Field, Input, PasswordInput } from '@immich/ui';
import { Alert, Button, Field, Input, PasswordInput, Stack } from '@immich/ui';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@ -101,15 +101,16 @@
{#if $featureFlags.loaded}
<AuthPageLayout title={data.meta.title}>
<Stack gap={4}>
{#if $serverConfig.loginPageMessage}
<Alert color="primary">
<Alert color="primary" class="mb-6">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $serverConfig.loginPageMessage}
</Alert>
{/if}
{#if !oauthLoading && $featureFlags.passwordLogin}
<form {onsubmit} class="mt-5 flex flex-col gap-5 text-dark mx-4">
<form {onsubmit} class="flex flex-col gap-4">
{#if errorMessage}
<Alert color="danger" title={errorMessage} closable />
{/if}
@ -128,7 +129,7 @@
{#if $featureFlags.oauth}
{#if $featureFlags.passwordLogin}
<div class="inline-flex w-full items-center justify-center mt-4">
<div class="inline-flex w-full items-center justify-center my-4">
<hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
<span
class="absolute left-1/2 -translate-x-1/2 bg-gray-50 px-3 font-medium text-gray-900 dark:bg-neutral-900 dark:text-white"
@ -137,7 +138,6 @@
</span>
</div>
{/if}
<div class="my-5 flex flex-col gap-5 mx-4">
{#if oauthError}
<Alert color="danger" title={oauthError} closable />
{/if}
@ -152,11 +152,11 @@
>
{$serverConfig.oauthButtonText}
</Button>
</div>
{/if}
{#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
<Alert color="warning" title={$t('login_has_been_disabled')} />
{/if}
</Stack>
</AuthPageLayout>
{/if}

View File

@ -47,13 +47,11 @@
</script>
<AuthPageLayout title={data.meta.title}>
<div class="mx-4 mt-4">
<Alert color="primary">
<form onsubmit={onSubmit} method="post" class="flex flex-col gap-4">
<Alert color="primary" class="mb-2">
<Text>{$t('admin.registration_description')}</Text>
</Alert>
</div>
<form onsubmit={onSubmit} method="post" class="mt-5 flex flex-col gap-5 text-dark p-4">
<Field label={$t('admin_email')} required>
<Input bind:value={email} type="email" autocomplete="email" />
</Field>
@ -74,8 +72,6 @@
<Alert color="danger" title={errorMessage} size="medium" class="mt-4" />
{/if}
<div class="my-5 flex w-full">
<Button type="submit" size="giant" shape="round" fullWidth disabled={!valid}>{$t('sign_up')}</Button>
</div>
<Button class="mt-4" type="submit" size="giant" shape="round" fullWidth disabled={!valid}>{$t('sign_up')}</Button>
</form>
</AuthPageLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Flower" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 792 792" style="enable-background:new 0 0 792 792;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FA2921;}
.st1{fill:#ED79B5;}
.st2{fill:#FFB400;}
.st3{fill:#1E83F7;}
.st4{fill:#18C249;}
</style>
<g id="Flower_00000077325900055813483940000000694823054982625702_">
<path class="st0" d="M375.48,267.63c38.64,34.21,69.78,70.87,89.82,105.42c34.42-61.56,57.42-134.71,57.71-181.3
c0-0.33,0-0.63,0-0.91c0-68.94-68.77-95.77-128.01-95.77s-128.01,26.83-128.01,95.77c0,0.94,0,2.2,0,3.72
C300.01,209.24,339.15,235.47,375.48,267.63z"/>
<path class="st1" d="M164.7,455.63c24.15-26.87,61.2-55.99,103.01-80.61c44.48-26.18,88.97-44.47,128.02-52.84
c-47.91-51.76-110.37-96.24-154.6-110.91c-0.31-0.1-0.6-0.19-0.86-0.28c-65.57-21.3-112.34,35.81-130.64,92.15
c-18.3,56.34-14.04,130.04,51.53,151.34C162.05,454.77,163.25,455.16,164.7,455.63z"/>
<path class="st2" d="M681.07,302.19c-18.3-56.34-65.07-113.45-130.64-92.15c-0.9,0.29-2.1,0.68-3.54,1.15
c-3.75,35.93-16.6,81.27-35.96,125.76c-20.59,47.32-45.84,88.27-72.51,118c69.18,13.72,145.86,12.98,190.26-1.14
c0.31-0.1,0.6-0.2,0.86-0.28C695.11,432.22,699.37,358.52,681.07,302.19z"/>
<path class="st3" d="M336.54,510.71c-11.15-50.39-14.8-98.36-10.7-138.08c-64.03,29.57-125.63,75.23-153.26,112.76
c-0.19,0.26-0.37,0.51-0.53,0.73c-40.52,55.78-0.66,117.91,47.27,152.72c47.92,34.82,119.33,53.54,159.86-2.24
c0.56-0.76,1.3-1.78,2.19-3.01C363.28,602.32,347.02,558.08,336.54,510.71z"/>
<path class="st4" d="M617.57,482.52c-35.33,7.54-82.42,9.33-130.72,4.66c-51.37-4.96-98.11-16.32-134.63-32.5
c8.33,70.03,32.73,142.73,59.88,180.6c0.19,0.26,0.37,0.51,0.53,0.73c40.52,55.78,111.93,37.06,159.86,2.24
c47.92-34.82,87.79-96.95,47.27-152.72C619.2,484.77,618.46,483.75,617.57,482.52z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB