Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran
7277ea3d7a feat(server): asset_user table 2025-01-28 22:04:21 -06:00
39 changed files with 139 additions and 165 deletions

6
cli/package-lock.json generated
View File

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

View File

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

View File

@@ -48,7 +48,6 @@ services:
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
devices:
- /dev/dri:/dev/dri
- /dev/dxg:/dev/dxg
volumes:
- /usr/lib/wsl:/usr/lib/wsl
environment:

View File

@@ -8,23 +8,22 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
## Image formats
| Format | Extension(s) | Supported? | Notes |
| :---------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
| Format | Extension(s) | Supported? | Notes |
| :-------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
## Video formats

View File

@@ -1,8 +1,4 @@
[
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"
},
{
"label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app"

8
e2e/package-lock.json generated
View File

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

View File

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

View File

@@ -356,24 +356,5 @@ describe('/admin/users', () => {
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should restore a user', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.post(`/admin/users/${user.userId}/restore`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: user.userId,
email: user.userEmail,
status: 'active',
deletedAt: null,
}),
);
});
});
});

View File

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

View File

@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release',
properties: {
"android.injected.version.code" => 182,
"android.injected.version.name" => "1.125.7",
"android.injected.version.name" => "1.125.6",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

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

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.125.7
- API version: 1.125.6
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -93,17 +93,17 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} |
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Checks if assets exist by checksums
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Checks if multiple assets exist on the server and returns all existing - used by background backup
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Get all asset of a device that are in the database, ID only.
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
*AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane |
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |

View File

@@ -16,8 +16,6 @@ class AssetsApi {
final ApiClient apiClient;
/// checkBulkUpload
///
/// Checks if assets exist by checksums
///
/// Note: This method returns the HTTP [Response].
@@ -50,8 +48,6 @@ class AssetsApi {
);
}
/// checkBulkUpload
///
/// Checks if assets exist by checksums
///
/// Parameters:
@@ -72,8 +68,6 @@ class AssetsApi {
return null;
}
/// checkExistingAssets
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
///
/// Note: This method returns the HTTP [Response].
@@ -106,8 +100,6 @@ class AssetsApi {
);
}
/// checkExistingAssets
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
///
/// Parameters:
@@ -223,8 +215,6 @@ class AssetsApi {
return null;
}
/// getAllUserAssetsByDeviceId
///
/// Get all asset of a device that are in the database, ID only.
///
/// Note: This method returns the HTTP [Response].
@@ -258,8 +248,6 @@ class AssetsApi {
);
}
/// getAllUserAssetsByDeviceId
///
/// Get all asset of a device that are in the database, ID only.
///
/// Parameters:
@@ -576,8 +564,6 @@ class AssetsApi {
return null;
}
/// replaceAsset
///
/// Replace the asset with new file, without changing its id
///
/// Note: This method returns the HTTP [Response].
@@ -659,8 +645,6 @@ class AssetsApi {
);
}
/// replaceAsset
///
/// Replace the asset with new file, without changing its id
///
/// Parameters:

View File

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

View File

@@ -539,7 +539,7 @@
}
],
"responses": {
"200": {
"201": {
"content": {
"application/json": {
"schema": {
@@ -1424,7 +1424,6 @@
},
"/assets/bulk-upload-check": {
"post": {
"description": "Checks if assets exist by checksums",
"operationId": "checkBulkUpload",
"parameters": [],
"requestBody": {
@@ -1460,7 +1459,7 @@
"api_key": []
}
],
"summary": "checkBulkUpload",
"summary": "Checks if assets exist by checksums",
"tags": [
"Assets"
]
@@ -1468,7 +1467,6 @@
},
"/assets/device/{deviceId}": {
"get": {
"description": "Get all asset of a device that are in the database, ID only.",
"operationId": "getAllUserAssetsByDeviceId",
"parameters": [
{
@@ -1506,7 +1504,7 @@
"api_key": []
}
],
"summary": "getAllUserAssetsByDeviceId",
"summary": "Get all asset of a device that are in the database, ID only.",
"tags": [
"Assets"
]
@@ -1514,7 +1512,6 @@
},
"/assets/exist": {
"post": {
"description": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"operationId": "checkExistingAssets",
"parameters": [],
"requestBody": {
@@ -1550,7 +1547,7 @@
"api_key": []
}
],
"summary": "checkExistingAssets",
"summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"tags": [
"Assets"
]
@@ -1906,7 +1903,6 @@
]
},
"put": {
"description": "Replace the asset with new file, without changing its id",
"operationId": "replaceAsset",
"parameters": [
{
@@ -1960,7 +1956,7 @@
"api_key": []
}
],
"summary": "replaceAsset",
"summary": "Replace the asset with new file, without changing its id",
"tags": [
"Assets"
],
@@ -7458,7 +7454,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.125.7",
"version": "1.125.6",
"contact": {}
},
"tags": [],

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.125.7
* 1.125.6
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -1475,7 +1475,7 @@ export function restoreUserAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
status: 201;
data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}/restore`, {
...opts,
@@ -1703,7 +1703,7 @@ export function updateAssets({ assetBulkUpdateDto }: {
})));
}
/**
* checkBulkUpload
* Checks if assets exist by checksums
*/
export function checkBulkUpload({ assetBulkUploadCheckDto }: {
assetBulkUploadCheckDto: AssetBulkUploadCheckDto;
@@ -1718,7 +1718,7 @@ export function checkBulkUpload({ assetBulkUploadCheckDto }: {
})));
}
/**
* getAllUserAssetsByDeviceId
* Get all asset of a device that are in the database, ID only.
*/
export function getAllUserAssetsByDeviceId({ deviceId }: {
deviceId: string;
@@ -1731,7 +1731,7 @@ export function getAllUserAssetsByDeviceId({ deviceId }: {
}));
}
/**
* checkExistingAssets
* Checks if multiple assets exist on the server and returns all existing - used by background backup
*/
export function checkExistingAssets({ checkExistingAssetsDto }: {
checkExistingAssetsDto: CheckExistingAssetsDto;
@@ -1839,7 +1839,7 @@ export function downloadAsset({ id, key }: {
}));
}
/**
* replaceAsset
* Replace the asset with new file, without changing its id
*/
export function replaceAsset({ id, key, assetMediaReplaceDto }: {
id: string;

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import {
UploadedFiles,
UseInterceptors,
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { EndpointLifecycle } from 'src/decorators';
import {
@@ -94,10 +94,6 @@ export class AssetMediaController {
@UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data')
@EndpointLifecycle({ addedAt: 'v1.106.0' })
@ApiOperation({
summary: 'replaceAsset',
description: 'Replace the asset with new file, without changing its id',
})
@Authenticated({ sharedLink: true })
async replaceAsset(
@Auth() auth: AuthDto,
@@ -145,10 +141,6 @@ export class AssetMediaController {
*/
@Post('exist')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'checkExistingAssets',
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
})
@Authenticated()
checkExistingAssets(
@Auth() auth: AuthDto,
@@ -162,10 +154,6 @@ export class AssetMediaController {
*/
@Post('bulk-upload-check')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'checkBulkUpload',
description: 'Checks if assets exist by checksums',
})
@Authenticated()
checkBulkUpload(
@Auth() auth: AuthDto,

View File

@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { EndpointLifecycle } from 'src/decorators';
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
import {
@@ -41,10 +41,6 @@ export class AssetController {
* Get all asset of a device that are in the database, ID only.
*/
@Get('/device/:deviceId')
@ApiOperation({
summary: 'getAllUserAssetsByDeviceId',
description: 'Get all asset of a device that are in the database, ID only.',
})
@Authenticated()
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId);

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
@@ -75,7 +75,6 @@ export class UserAdminController {
@Post(':id/restore')
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
@HttpCode(HttpStatus.OK)
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id);
}

28
server/src/db.d.ts vendored
View File

@@ -3,21 +3,16 @@
* Please do not edit it manually.
*/
import type { ColumnType } from "kysely";
import type { ColumnType } from 'kysely';
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[]
? U[]
: ArrayTypeImpl<T>;
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S[], I[], U[]>
: T[];
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S[], I[], U[]> : T[];
export type AssetsStatusEnum = "active" | "deleted" | "trashed";
export type AssetsStatusEnum = 'active' | 'deleted' | 'trashed';
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
@@ -33,7 +28,7 @@ export type JsonPrimitive = boolean | number | string | null;
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
export type Sourcetype = "exif" | "machine-learning";
export type Sourcetype = 'exif' | 'machine-learning';
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
@@ -154,6 +149,12 @@ export interface AssetStack {
primaryAssetId: string;
}
export interface AssetUser {
assetId: string;
createdAt: Timestamp;
userId: string;
}
export interface Audit {
action: string;
createdAt: Generated<Timestamp>;
@@ -413,6 +414,7 @@ export interface DB {
asset_files: AssetFiles;
asset_job_status: AssetJobStatus;
asset_stack: AssetStack;
asset_user: AssetUser;
assets: Assets;
audit: Audit;
exif: Exif;
@@ -438,6 +440,6 @@ export interface DB {
tags_closure: TagsClosure;
user_metadata: UserMetadata;
users: Users;
"vectors.pg_vector_index_stat": VectorsPgVectorIndexStat;
'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat;
version_history: VersionHistory;
}

View File

@@ -0,0 +1,22 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { UserEntity } from 'src/entities/user.entity';
import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm';
@Entity('asset_user')
@Index('IDX_assetId_userId', ['assetId', 'userId'])
export class AssetUserEntity {
@PrimaryColumn()
assetId!: string;
@ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
asset!: AssetEntity;
@PrimaryColumn()
userId!: string;
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
user!: UserEntity;
@Column()
createdAt!: Date;
}

View File

@@ -5,6 +5,7 @@ import { APIKeyEntity } from 'src/entities/api-key.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetUserEntity } from 'src/entities/asset-user.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { AuditEntity } from 'src/entities/audit.entity';
import { ExifEntity } from 'src/entities/exif.entity';
@@ -34,6 +35,7 @@ export const entities = [
AssetEntity,
AssetFaceEntity,
AssetFileEntity,
AssetUserEntity,
AssetJobStatusEntity,
AuditEntity,
ExifEntity,

View File

@@ -36,7 +36,6 @@ export interface IUserRepository {
getUserStats(): Promise<UserStatsQueryResponse[]>;
create(user: Insertable<Users>): Promise<UserEntity>;
update(id: string, user: Updateable<Users>): Promise<UserEntity>;
restore(id: string): Promise<UserEntity>;
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;

View File

@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAssetUserTable1738099775096 implements MigrationInterface {
name = 'AddAssetUserTable1738099775096'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "asset_user" ("assetId" uuid NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_f3d7f17ab93d60e007282726058" PRIMARY KEY ("assetId", "userId"))`);
await queryRunner.query(`CREATE INDEX "IDX_assetId_userId" ON "asset_user" ("assetId", "userId") `);
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_07c8478e0936e78b553aaeceedb" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2"`);
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_07c8478e0936e78b553aaeceedb"`);
await queryRunner.query(`DROP INDEX "public"."IDX_assetId_userId"`);
await queryRunner.query(`DROP TABLE "asset_user"`);
}
}

View File

@@ -81,7 +81,24 @@ export class AssetRepository implements IAssetRepository {
}
create(asset: Insertable<Assets>): Promise<AssetEntity> {
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
return this.db.transaction().execute(async (tx) => {
const newAsset = (await tx
.insertInto('assets')
.values(asset)
.returningAll()
.executeTakeFirst()) as any as AssetEntity;
await tx
.insertInto('asset_user')
.values({
assetId: newAsset.id,
userId: newAsset.ownerId,
createdAt: new Date(),
})
.execute();
return newAsset;
});
}
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })

View File

@@ -5,7 +5,6 @@ import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { UserMetadata } from 'src/entities/user-metadata.entity';
import { UserEntity, withMetadata } from 'src/entities/user.entity';
import { UserStatus } from 'src/enum';
import {
IUserRepository,
UserFindOptions,
@@ -141,16 +140,6 @@ export class UserRepository implements IUserRepository {
.executeTakeFirst() as unknown as Promise<UserEntity>;
}
restore(id: string): Promise<UserEntity> {
return this.db
.updateTable('users')
.set({ status: UserStatus.ACTIVE, deletedAt: null })
.where('users.id', '=', asUuid(id))
.returning(columns)
.returning(withMetadata)
.executeTakeFirst() as unknown as Promise<UserEntity>;
}
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
await this.db
.insertInto('user_metadata')

View File

@@ -73,7 +73,6 @@ const validImages = [
'.heic',
'.heif',
'.iiq',
'.jp2',
'.jpeg',
'.jpg',
'.jxl',

View File

@@ -173,9 +173,9 @@ describe(UserAdminService.name, () => {
it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1.id);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
});
});
});

View File

@@ -102,7 +102,7 @@ export class UserAdminService extends BaseService {
async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
await this.findOrFail(id, { withDeleted: true });
await this.albumRepository.restoreAll(id);
const user = await this.userRepository.restore(id);
const user = await this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE });
return mapUserAdmin(user);
}

View File

@@ -22,7 +22,6 @@ describe('mimeTypes', () => {
{ mimetype: 'image/heif', extension: '.heif' },
{ mimetype: 'image/hif', extension: '.hif' },
{ mimetype: 'image/iiq', extension: '.iiq' },
{ mimetype: 'image/jp2', extension: '.jp2' },
{ mimetype: 'image/jpeg', extension: '.jpe' },
{ mimetype: 'image/jpeg', extension: '.jpeg' },
{ mimetype: 'image/jpeg', extension: '.jpg' },

View File

@@ -43,7 +43,6 @@ const image: Record<string, string[]> = {
'.heif': ['image/heif'],
'.hif': ['image/hif'],
'.insp': ['image/jpeg'],
'.jp2': ['image/jp2'],
'.jpe': ['image/jpeg'],
'.jpeg': ['image/jpeg'],
'.jpg': ['image/jpeg'],

View File

@@ -13,7 +13,6 @@ export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
restore: vitest.fn(),
getDeletedUsers: vitest.fn(),
hasAdmin: vitest.fn(),
updateUsage: vitest.fn(),

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.125.7",
"version": "1.125.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.125.7",
"version": "1.125.6",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -77,7 +77,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.125.7",
"version": "1.125.6",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

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

View File

@@ -35,7 +35,6 @@
locale,
type AlbumViewSettings,
} from '$lib/stores/preferences.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { t } from 'svelte-i18n';
@@ -294,15 +293,6 @@
sharedAlbums[sharedAlbums.findIndex(({ id }) => id === album.id)] = album;
};
const updateRecentAlbumInfo = (album: AlbumResponseDto) => {
for (const cachedAlbum of userInteraction.recentAlbums || []) {
if (cachedAlbum.id === album.id) {
Object.assign(cachedAlbum, { ...cachedAlbum, ...album });
break;
}
}
};
const successEditAlbumInfo = (album: AlbumResponseDto) => {
albumToEdit = null;
@@ -318,7 +308,6 @@
});
updateAlbumInfo(album);
updateRecentAlbumInfo(album);
};
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {