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 needs: build_mobile
steps: 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 - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
token: ${{ secrets.ORG_RELEASE_TOKEN }} token: ${{ steps.generate-token.outputs.token }}
- name: Download APK - name: Download APK
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4

6
cli/package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.40", "version": "2.2.42",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "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", "label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app" "url": "https://v1.124.2.archive.immich.app"

8
e2e/package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.124.2", "version": "1.125.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "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", "latest_version": "Latest Version",
"latitude": "Latitude", "latitude": "Latitude",
"leave": "Leave", "leave": "Leave",
"lens_model": "Lens model",
"let_others_respond": "Let others respond", "let_others_respond": "Let others respond",
"level": "Level", "level": "Level",
"library": "Library", "library": "Library",
@ -1113,6 +1114,7 @@
"search_camera_model": "Search camera model...", "search_camera_model": "Search camera model...",
"search_city": "Search city...", "search_city": "Search city...",
"search_country": "Search country...", "search_country": "Search country...",
"search_for": "Search for",
"search_for_existing_person": "Search for existing person", "search_for_existing_person": "Search for existing person",
"search_no_people": "No people", "search_no_people": "No people",
"search_no_people_named": "No people named \"{name}\"", "search_no_people_named": "No people named \"{name}\"",

View File

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

View File

@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 175, "android.injected.version.code" => 177,
"android.injected.version.name" => "1.124.2", "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') 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" desc "iOS Release"
lane :release do lane :release do
increment_version_number( increment_version_number(
version_number: "1.124.2" version_number: "1.125.1"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, 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: 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 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - 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 description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.124.2+175 version: 1.125.1+177
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react'; import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component'; import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout'; 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'; import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const AlbumInviteEmail = ({ export const AlbumInviteEmail = ({

View File

@ -2,7 +2,7 @@ import { Img, Link, Section, Text } from '@react-email/components';
import * as React from 'react'; import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component'; import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout'; 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'; import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const AlbumUpdateEmail = ({ export const AlbumUpdateEmail = ({

View File

@ -1,7 +1,7 @@
import { Link, Row, Text } from '@react-email/components'; import { Link, Row, Text } from '@react-email/components';
import * as React from 'react'; import * as React from 'react';
import ImmichLayout from 'src/emails/components/immich.layout'; 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) => ( export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => (
<ImmichLayout preview="This is a test email from Immich."> <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 * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component'; import { ImmichButton } from 'src/emails/components/button.component';
import ImmichLayout from 'src/emails/components/immich.layout'; 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'; import { replaceTemplateTags } from 'src/utils/replace-template-tags';
export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => { 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 { ClassConstructor } from 'class-transformer';
import { EmailImageAttachment } from 'src/interfaces/notification.interface'; import { EmailImageAttachment } from 'src/repositories/notification.repository';
export enum QueueName { export enum QueueName {
THUMBNAIL_GENERATION = 'thumbnailGeneration', 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 from
( (
select select
* "assets".*,
to_json("exifInfo") as "exifInfo"
from from
"assets" "assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where where
"assets"."deletedAt" is null "assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id" and "assets"."stackId" = "asset_stack"."id"
@ -31,7 +40,7 @@ select
from from
( (
select select
*, "assets".*,
( (
select select
coalesce(json_agg(agg), '[]') coalesce(json_agg(agg), '[]')
@ -45,9 +54,18 @@ select
where where
"tag_asset"."assetsId" = "assets"."id" "tag_asset"."assetsId" = "assets"."id"
) as agg ) as agg
) as "tags" ) as "tags",
to_json("exifInfo") as "exifInfo"
from from
"assets" "assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where where
"assets"."deletedAt" is null "assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id" and "assets"."stackId" = "asset_stack"."id"
@ -67,7 +85,7 @@ select
from from
( (
select select
*, "assets".*,
( (
select select
coalesce(json_agg(agg), '[]') coalesce(json_agg(agg), '[]')
@ -81,9 +99,18 @@ select
where where
"tag_asset"."assetsId" = "assets"."id" "tag_asset"."assetsId" = "assets"."id"
) as agg ) as agg
) as "tags" ) as "tags",
to_json("exifInfo") as "exifInfo"
from from
"assets" "assets"
inner join lateral (
select
"exif".*
from
"exif"
where
"exif"."assetId" = "assets"."id"
) as "exifInfo" on true
where where
"assets"."deletedAt" is null "assets"."deletedAt" is null
and "assets"."stackId" = "asset_stack"."id" and "assets"."stackId" = "asset_stack"."id"

View File

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

View File

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

View File

@ -1,11 +1,24 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule'; import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob, CronTime } from 'cron'; import { CronJob, CronTime } from 'cron';
import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface';
import { LoggingRepository } from 'src/repositories/logging.repository'; 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() @Injectable()
export class CronRepository implements ICronRepository { export class CronRepository {
constructor( constructor(
private schedulerRegistry: SchedulerRegistry, private schedulerRegistry: SchedulerRegistry,
private logger: LoggingRepository, 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 { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface'; import { IJobRepository } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.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 { 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 { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface'; import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.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 { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository'; import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@ -71,44 +61,44 @@ import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [ export const repositories = [
AccessRepository, AccessRepository,
ActivityRepository, ActivityRepository,
AlbumUserRepository,
AuditRepository, AuditRepository,
ApiKeyRepository, ApiKeyRepository,
ConfigRepository, ConfigRepository,
CronRepository,
LoggingRepository, LoggingRepository,
MapRepository,
MediaRepository, MediaRepository,
MemoryRepository, MemoryRepository,
MetadataRepository,
NotificationRepository,
OAuthRepository,
ServerInfoRepository,
TelemetryRepository,
TrashRepository,
ViewRepository, ViewRepository,
VersionHistoryRepository,
]; ];
export const providers = [ export const providers = [
{ provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAlbumRepository, useClass: AlbumRepository },
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository }, { provide: IAssetRepository, useClass: AssetRepository },
{ provide: ICronRepository, useClass: CronRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository }, { provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDatabaseRepository, useClass: DatabaseRepository }, { provide: IDatabaseRepository, useClass: DatabaseRepository },
{ provide: IEventRepository, useClass: EventRepository }, { provide: IEventRepository, useClass: EventRepository },
{ provide: IJobRepository, useClass: JobRepository }, { provide: IJobRepository, useClass: JobRepository },
{ provide: ILibraryRepository, useClass: LibraryRepository }, { provide: ILibraryRepository, useClass: LibraryRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMapRepository, useClass: MapRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository }, { provide: IMoveRepository, useClass: MoveRepository },
{ provide: INotificationRepository, useClass: NotificationRepository },
{ provide: IOAuthRepository, useClass: OAuthRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository }, { provide: IPersonRepository, useClass: PersonRepository },
{ provide: IProcessRepository, useClass: ProcessRepository }, { provide: IProcessRepository, useClass: ProcessRepository },
{ provide: ISearchRepository, useClass: SearchRepository }, { provide: ISearchRepository, useClass: SearchRepository },
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISessionRepository, useClass: SessionRepository }, { provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository }, { provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository }, { provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository }, { provide: ITagRepository, useClass: TagRepository },
{ provide: ITelemetryRepository, useClass: TelemetryRepository },
{ provide: ITrashRepository, useClass: TrashRepository },
{ provide: IUserRepository, useClass: UserRepository }, { 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 { AssetEntity, withExif } from 'src/entities/asset.entity';
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
import { LogLevel, SystemMetadataKey } from 'src/enum'; 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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.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 { interface MapDB extends DB {
geodata_places_tmp: GeodataPlaces; geodata_places_tmp: GeodataPlaces;
naturalearth_countries_tmp: NaturalearthCountries; naturalearth_countries_tmp: NaturalearthCountries;
} }
@Injectable() @Injectable()
export class MapRepository implements IMapRepository { export class MapRepository {
constructor( constructor(
private configRepository: ConfigRepository, private configRepository: ConfigRepository,
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository, @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,

View File

@ -1,11 +1,72 @@
import { Injectable } from '@nestjs/common'; 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 geotz from 'geo-tz';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { LoggingRepository } from 'src/repositories/logging.repository'; 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() @Injectable()
export class MetadataRepository implements IMetadataRepository { export class MetadataRepository {
private exiftool = new ExifTool({ private exiftool = new ExifTool({
defaultVideosToUTC: true, defaultVideosToUTC: true,
backfillTimezones: 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 { 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 { ILoggingRepository } from 'src/types';
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest'; 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 { AlbumUpdateEmail } from 'src/emails/album-update.email';
import { TestEmail } from 'src/emails/test.email'; import { TestEmail } from 'src/emails/test.email';
import { WelcomeEmail } from 'src/emails/welcome.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'; 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() @Injectable()
export class NotificationRepository implements INotificationRepository { export class NotificationRepository {
constructor(private logger: LoggingRepository) { constructor(private logger: LoggingRepository) {
this.logger.setContext(NotificationRepository.name); this.logger.setContext(NotificationRepository.name);
} }

View File

@ -1,10 +1,21 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { custom, generators, Issuer } from 'openid-client'; import { custom, generators, Issuer, UserinfoResponse } from 'openid-client';
import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface';
import { LoggingRepository } from 'src/repositories/logging.repository'; 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() @Injectable()
export class OAuthRepository implements IOAuthRepository { export class OAuthRepository {
constructor(private logger: LoggingRepository) { constructor(private logger: LoggingRepository) {
this.logger.setContext(OAuthRepository.name); this.logger.setContext(OAuthRepository.name);
} }

View File

@ -133,10 +133,6 @@ export class PersonRepository implements IPersonRepository {
) )
.where('person.ownerId', '=', userId) .where('person.ownerId', '=', userId)
.orderBy('person.isHidden', 'asc') .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) => .having((eb) =>
eb.or([ eb.or([
eb('person.name', '!=', ''), 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)) .$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
.offset(pagination.skip ?? 0) .offset(pagination.skip ?? 0)
.limit(pagination.take + 1) .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 { readFile } from 'node:fs/promises';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import sharp from 'sharp'; import sharp from 'sharp';
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.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 exec = promisify(execCallback);
const maybeFirstLine = async (command: string): Promise<string> => { const maybeFirstLine = async (command: string): Promise<string> => {
try { try {
@ -34,7 +51,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
}; };
@Injectable() @Injectable()
export class ServerInfoRepository implements IServerInfoRepository { export class ServerInfoRepository {
constructor( constructor(
private configRepository: ConfigRepository, private configRepository: ConfigRepository,
private logger: LoggingRepository, private logger: LoggingRepository,

View File

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

View File

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

View File

@ -3,9 +3,8 @@ import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetStatus } from 'src/enum'; 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>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
getDeletedIds(): AsyncIterableIterator<{ id: string }> { getDeletedIds(): AsyncIterableIterator<{ id: string }> {

View File

@ -3,25 +3,23 @@ import { Insertable, Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB, VersionHistory } from 'src/db'; import { DB, VersionHistory } from 'src/db';
import { GenerateSql } from 'src/decorators'; import { GenerateSql } from 'src/decorators';
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
@Injectable() @Injectable()
export class VersionHistoryRepository implements IVersionHistoryRepository { export class VersionHistoryRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql() @GenerateSql()
getAll(): Promise<VersionHistoryEntity[]> { getAll() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute(); return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute();
} }
@GenerateSql() @GenerateSql()
getLatest(): Promise<VersionHistoryEntity | undefined> { getLatest() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst(); return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
} }
@GenerateSql({ params: [{ version: 'v1.123.0' }] }) @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(); 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 _ from 'lodash';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { AlbumUserRole } from 'src/enum'; import { AlbumUserRole } from 'src/enum';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AlbumService } from 'src/services/album.service'; import { AlbumService } from 'src/services/album.service';
import { IAlbumUserRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub'; import { albumStub } from 'test/fixtures/album.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.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 { AuthType, Permission } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service'; 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 { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub'; import { sessionStub } from 'test/fixtures/session.stub';

View File

@ -19,7 +19,7 @@ import {
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum'; 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 { BaseService } from 'src/services/base.service';
import { AuthApiKey } from 'src/types'; import { AuthApiKey } from 'src/types';
import { isGranted } from 'src/utils/access'; import { isGranted } from 'src/utils/access';

View File

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

View File

@ -3,10 +3,10 @@ import { defaults, SystemConfig } from 'src/config';
import { ImmichWorker } from 'src/enum'; import { ImmichWorker } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.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 { JobService } from 'src/services/job.service';
import { IConfigRepository, ILoggingRepository } from 'src/types'; import { IConfigRepository, ILoggingRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { ITelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newTestService } from 'test/utils'; import { newTestService } from 'test/utils';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
@ -16,7 +16,7 @@ describe(JobService.name, () => {
let configMock: Mocked<IConfigRepository>; let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggingRepository>; let loggerMock: Mocked<ILoggingRepository>;
let telemetryMock: Mocked<ITelemetryRepository>; let telemetryMock: ITelemetryRepositoryMock;
beforeEach(() => { beforeEach(() => {
({ sut, assetMock, configMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {})); ({ 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 { UserEntity } from 'src/entities/user.entity';
import { AssetType, ImmichWorker } from 'src/enum'; import { AssetType, ImmichWorker } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { import {
IJobRepository, IJobRepository,
@ -18,7 +17,7 @@ import {
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { LibraryService } from 'src/services/library.service'; 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 { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { libraryStub } from 'test/fixtures/library.stub'; import { libraryStub } from 'test/fixtures/library.stub';

View File

@ -1,7 +1,7 @@
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { MapService } from 'src/services/map.service'; import { MapService } from 'src/services/map.service';
import { IMapRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub'; import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.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 { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface'; import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service'; 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 { assetStub } from 'test/fixtures/asset.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { probeStub } from 'test/fixtures/media.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 { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface'; import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { ReverseGeocodeResult } from 'src/interfaces/map.interface'; import { ReverseGeocodeResult } from 'src/repositories/map.repository';
import { ImmichTags } from 'src/interfaces/metadata.interface'; import { ImmichTags } from 'src/repositories/metadata.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { isFaceImportEnabled } from 'src/utils/misc'; import { isFaceImportEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination'; 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 { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { NotificationService } from 'src/services/notification.service'; import { NotificationService } from 'src/services/notification.service';
import { INotificationRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub'; import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';

View File

@ -12,7 +12,7 @@ import {
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } 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 { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file'; import { getFilenameExtension } from 'src/utils/file';

View File

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

View File

@ -4,11 +4,9 @@ import { serverVersion } from 'src/constants';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { VersionService } from 'src/services/version.service'; 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 { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils'; import { newTestService } from 'test/utils';
import { Mocked } from 'vitest'; 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 { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.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 { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AuditRepository } from 'src/repositories/audit.repository'; import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { CronRepository } from 'src/repositories/cron.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository'; import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.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'; import { ViewRepository } from 'src/repositories/view-repository';
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; 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 IActivityRepository = RepositoryInterface<ActivityRepository>;
export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> }; export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface<AccessRepository[K]> };
export type IAlbumUserRepository = RepositoryInterface<AlbumUserRepository>;
export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>; export type IApiKeyRepository = RepositoryInterface<ApiKeyRepository>;
export type IAuditRepository = RepositoryInterface<AuditRepository>; export type IAuditRepository = RepositoryInterface<AuditRepository>;
export type IConfigRepository = RepositoryInterface<ConfigRepository>; export type IConfigRepository = RepositoryInterface<ConfigRepository>;
export type ICronRepository = RepositoryInterface<CronRepository>;
export type ILoggingRepository = Pick< export type ILoggingRepository = Pick<
LoggingRepository, LoggingRepository,
| 'verbose' | 'verbose'
@ -39,9 +51,18 @@ export type ILoggingRepository = Pick<
| 'setContext' | 'setContext'
| 'setAppName' | 'setAppName'
>; >;
export type IMapRepository = RepositoryInterface<MapRepository>;
export type IMediaRepository = RepositoryInterface<MediaRepository>; export type IMediaRepository = RepositoryInterface<MediaRepository>;
export type IMemoryRepository = RepositoryInterface<MemoryRepository>; 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 IViewRepository = RepositoryInterface<ViewRepository>;
export type IVersionHistoryRepository = RepositoryInterface<VersionHistoryRepository>;
export type ActivityItem = export type ActivityItem =
| Awaited<ReturnType<IActivityRepository['create']>> | 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'; import { personStub } from 'test/fixtures/person.stub';
export const metadataStub = { 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'; import { Mocked } from 'vitest';
export const newAlbumUserRepositoryMock = (): Mocked<IAlbumUserRepository> => { 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'; import { Mocked, vitest } from 'vitest';
export const newCronRepositoryMock = (): Mocked<ICronRepository> => { 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'; import { Mocked } from 'vitest';
export const newMapRepositoryMock = (): Mocked<IMapRepository> => { 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'; import { Mocked, vitest } from 'vitest';
export const newMetadataRepositoryMock = (): Mocked<IMetadataRepository> => { 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'; import { Mocked } from 'vitest';
export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => { 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'; import { Mocked } from 'vitest';
export const newOAuthRepositoryMock = (): Mocked<IOAuthRepository> => { 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'; import { Mocked, vitest } from 'vitest';
export const newServerInfoRepositoryMock = (): Mocked<IServerInfoRepository> => { 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'; import { Mocked, vitest } from 'vitest';
const newMetricGroupMock = () => { 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 { return {
setup: vitest.fn(), setup: vitest.fn(),
api: newMetricGroupMock(), 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'; import { Mocked, vitest } from 'vitest';
export const newTrashRepositoryMock = (): Mocked<ITrashRepository> => { 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'; import { Mocked, vitest } from 'vitest';
export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => { export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {

View File

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

8
web/package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.124.2", "version": "1.125.1",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"scripts": { "scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000", "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 UserAvatar from '../shared-components/user-avatar.svelte';
import AlbumListItemDetails from './album-list-item-details.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
interface Props { interface Props {
asset: AssetResponseDto; asset: AssetResponseDto;
@ -410,7 +411,36 @@
<div><Icon path={mdiCameraIris} size="24" /></div> <div><Icon path={mdiCameraIris} size="24" /></div>
<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"> <div class="flex gap-2 text-sm">
{#if asset.exifInfo?.fNumber} {#if asset.exifInfo?.fNumber}
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p> <p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>

View File

@ -10,7 +10,12 @@
let { title, children }: Props = $props(); let { title, children }: Props = $props();
</script> </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"> <Card color="secondary" class="w-full max-w-lg border m-2">
<CardHeader class="mt-6"> <CardHeader class="mt-6">
<VStack> <VStack>
@ -18,7 +23,8 @@
<Heading size="large" class="font-semibold" color="primary">{title}</Heading> <Heading size="large" class="font-semibold" color="primary">{title}</Heading>
</VStack> </VStack>
</CardHeader> </CardHeader>
<CardBody>
<CardBody class="p-8">
{@render children?.()} {@render children?.()}
</CardBody> </CardBody>
</Card> </Card>

View File

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

View File

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

View File

@ -6,7 +6,7 @@
import { oauth } from '$lib/utils'; import { oauth } from '$lib/utils';
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
import { login } from '@immich/sdk'; 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 { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -101,15 +101,16 @@
{#if $featureFlags.loaded} {#if $featureFlags.loaded}
<AuthPageLayout title={data.meta.title}> <AuthPageLayout title={data.meta.title}>
<Stack gap={4}>
{#if $serverConfig.loginPageMessage} {#if $serverConfig.loginPageMessage}
<Alert color="primary"> <Alert color="primary" class="mb-6">
<!-- eslint-disable-next-line svelte/no-at-html-tags --> <!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $serverConfig.loginPageMessage} {@html $serverConfig.loginPageMessage}
</Alert> </Alert>
{/if} {/if}
{#if !oauthLoading && $featureFlags.passwordLogin} {#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} {#if errorMessage}
<Alert color="danger" title={errorMessage} closable /> <Alert color="danger" title={errorMessage} closable />
{/if} {/if}
@ -128,7 +129,7 @@
{#if $featureFlags.oauth} {#if $featureFlags.oauth}
{#if $featureFlags.passwordLogin} {#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" /> <hr class="my-4 h-px w-3/4 border-0 bg-gray-200 dark:bg-gray-600" />
<span <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" 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> </span>
</div> </div>
{/if} {/if}
<div class="my-5 flex flex-col gap-5 mx-4">
{#if oauthError} {#if oauthError}
<Alert color="danger" title={oauthError} closable /> <Alert color="danger" title={oauthError} closable />
{/if} {/if}
@ -152,11 +152,11 @@
> >
{$serverConfig.oauthButtonText} {$serverConfig.oauthButtonText}
</Button> </Button>
</div>
{/if} {/if}
{#if !$featureFlags.passwordLogin && !$featureFlags.oauth} {#if !$featureFlags.passwordLogin && !$featureFlags.oauth}
<Alert color="warning" title={$t('login_has_been_disabled')} /> <Alert color="warning" title={$t('login_has_been_disabled')} />
{/if} {/if}
</Stack>
</AuthPageLayout> </AuthPageLayout>
{/if} {/if}

View File

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