Compare commits

...

13 Commits

Author SHA1 Message Date
timonrieger 131b5f1937 gen client 2026-06-08 20:42:55 +02:00
timonrieger 03ef4b385b bump openapi-generator version to v7.23.0 2026-06-08 20:42:55 +02:00
timonrieger 1d00e6a420 remove patch in favor of [OpenAPITools/openapi-generator#23930](https://github.com/OpenAPITools/openapi-generator/issues/23930)
partially revert "chore(mobile): make openapi requests abortable (#28692)"
2026-06-08 20:42:55 +02:00
timonrieger 39f142d5cd remove patch in favor of OpenAPITools/openapi-generator#23365 2026-06-08 20:42:55 +02:00
timonrieger 82a24f0583 remove patch in favor of OpenAPITools/openapi-generator#23811 2026-06-08 20:42:55 +02:00
bo0tzz 70bb7e4b7e fix: step name reference in fix-format.yml (#28912) 2026-06-08 14:32:34 -04:00
Yaros f973927c68 docs: replace make for mise (#28913)
* docs: replace make for mise

* chore: remove makefile comment
2026-06-08 14:31:23 -04:00
Daniel Dietzler e29267359e fix: detail panel faces reactivity issues (#28910) 2026-06-08 18:07:57 +02:00
joojoooo 164cda87a3 fix(web): use irot/imir tags for HEIF Orientation (#27820)
* fix(web): use irot/imir tags for HEIF Orientation

* ignore Exif Orientation for HEIF images per MIAF standard compliance

* add Rotation and Mirroring to exiftool numericTags

* add isHeifBasedImage function to detect HEIF-based image extensions

* add getHeifBasedOrientation method to map irot/imir tags to ExifOrientation

* removed mirroring, simplified code

* Removed "Based" in "heifBased"

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-06-08 09:33:28 -04:00
renovate[bot] 12d344efe0 chore(deps): update pnpm to v11 (#28773)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-08 14:44:45 +02:00
Timon 474efd39f8 refactor(server): prevent sharing album with owner by filtering out user from albumUsers (#28891)
fix(server): prevent sharing album with owner by filtering out user from albumUsers
2026-06-07 17:46:26 -04:00
Timon 9e453440e6 refactor(server): deprecate PUT routes in favor of PATCH (#28859)
* add patch routes and deprecate put

* gen client
2026-06-07 09:40:01 -04:00
Timon 8860817c76 chore: global Java (#28874) 2026-06-07 09:36:28 -04:00
49 changed files with 1394 additions and 450 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
if: always()
with:
github-token: ${{ steps.generate-token.outputs.token }}
github-token: ${{ steps.token.outputs.token }}
script: |
github.rest.issues.removeLabel({
issue_number: context.payload.pull_request.number,
+1 -1
View File
@@ -7,7 +7,7 @@ Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generat
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server **when running in development mode**. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the server in development mode and run the command below to update the client SDK.
```bash
make open-api
mise open-api
```
You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
+1 -1
View File
@@ -218,7 +218,7 @@ When the Dev Container starts, it automatically:
- Debug ports: 9230 (workers), 9231 (API)
:::info
The Dev Container setup replaces the `make dev` command from the traditional setup. All services start automatically when you open the container.
The Dev Container setup replaces the `mise dev` command from the traditional setup. All services start automatically when you open the container.
:::
### Accessing Services
+1 -1
View File
@@ -2,7 +2,7 @@
A minimal devcontainer is supplied with this repository. All commands can be executed directly inside this container to avoid tedious installation of the environment.
:::warning
The provided devcontainer isn't complete at the moment. At least all dockerized steps in the Makefile won't work (`make dev`, ....). Feel free to contribute!
The provided devcontainer isn't complete at the moment. At least all dockerized steps in the Makefile won't work (`mise dev`, ....). Feel free to contribute!
:::
When contributing code through a pull request, please check the following:
+2 -2
View File
@@ -45,7 +45,7 @@ All the services are packaged to run as with single Docker Compose command.
5. From the root directory, run:
```bash title="Start development server"
make dev # required Makefile installed on the system.
mise dev
```
5. Access the dev instance in your browser at http://localhost:3000, or connect via the mobile app.
@@ -88,7 +88,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`)
4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`)
5. Uncomment the import statement in `web/src/app.css` file `@import '../../../ui/packages/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
6. Start up the stack via `make dev`
6. Start up the stack via `mise dev`
7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`)
### Mobile app
+1 -1
View File
@@ -12,7 +12,7 @@ You need to run `mise //server:install` before _once_.
The e2e tests can be run by first starting up a test production environment via:
```bash
make e2e
mise e2e
```
Before you can run the tests, you need to run the following commands _once_:
+2 -1
View File
@@ -4,7 +4,8 @@ services:
e2e-auth-server:
container_name: immich-e2e-auth-server
build:
context: ../packages/e2e-auth-server
context: ../
dockerfile: packages/e2e-auth-server/Dockerfile
ports:
- 2286:2286
+4 -3
View File
@@ -504,13 +504,14 @@ describe('/albums', () => {
});
});
it('should not be able to share album with owner', async () => {
it('should deduplicate owner from albumUsers on create', async () => {
const { status, body } = await request(app)
.post('/albums')
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
expect(status).toBe(201);
expect(body.albumUsers).toHaveLength(1);
expect(body.albumUsers[0]).toMatchObject({ role: AlbumUserRole.Owner, user: { id: user1.userId } });
});
});
+45 -51
View File
@@ -82,40 +82,8 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
@@ -156,6 +124,30 @@ checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.node]]
version = "24.15.0"
backend = "core:node"
@@ -225,36 +217,38 @@ checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c70773
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.pnpm]]
version = "10.33.4"
version = "11.4.0"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:d29649c7380b5cd522f574208fbd35335846686498f45004604d3f5b8658b5cb"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-arm64"
checksum = "sha256:cc38ebd5b2610a5744f84576b963c49e6609a8df5aed714ae3de749998d4478c"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:d29649c7380b5cd522f574208fbd35335846686498f45004604d3f5b8658b5cb"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-arm64"
checksum = "sha256:a1e2ec9123c709fd04b704227cfcf3b50cd2bbbc1bd39d2df414530b5697eb75"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-arm64-musl.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:ff1795595535a10d0dfe327303f3dd02377be141190b1f5756de68edde2cf813"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-x64"
checksum = "sha256:f3f8d1217eef013bbc71a24d52efb1f1041e4aff55edd80e0b08e25f409305a4"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:ff1795595535a10d0dfe327303f3dd02377be141190b1f5756de68edde2cf813"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-linux-x64"
checksum = "sha256:60010ad00a96b71e20d1618acaca7a71395e710cbd5e88946c030a1d07c56916"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-linux-x64-musl.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:7aae186a04e1ffaa0047d43cd07d68a98dec303304f28be52234ba955d26c671"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-macos-arm64"
[tools.pnpm."platforms.macos-x64"]
checksum = "sha256:3b0c97b9f794cdda293949a8ee0e0151ca08f512f4a832408386221c7c73eec6"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-macos-x64"
checksum = "sha256:ba59014c2c1ce8b76af9f559385206a2623de4ff2b694b5c91598a8f44abb4e2"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-darwin-arm64.tar.gz"
provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:3268b2f29defe0dce8a3a26c0ef01488f0d4aa4872923173186ef618ab7d68ef"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.4/pnpm-win-x64.exe"
checksum = "sha256:84ce90e38bc0b1164173eb853a0fbffc7edcb050cb0d5c8ce4ca609f5c808e0a"
url = "https://github.com/pnpm/pnpm/releases/download/v11.4.0/pnpm-win32-x64.zip"
provenance = "github-attestations"
[[tools.terragrunt]]
version = "1.0.3"
+2 -1
View File
@@ -16,13 +16,14 @@ config_roots = [
[tools]
node = "24.15.0"
pnpm = "10.33.4"
pnpm = "11.4.0"
terragrunt = "1.0.3"
opentofu = "1.11.6"
"npm:oazapfts" = "7.5.0"
"github:extism/cli" = "1.6.3"
"github:webassembly/binaryen" = "version_124"
"github:extism/js-pdk" = "1.6.0"
java = "21.0.2"
[tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6"
+1 -1
View File
@@ -1 +1 @@
7.22.0
7.23.0
+15 -1
View File
@@ -4,7 +4,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 3.0.0
- Generator version: 7.22.0
- Generator version: 7.23.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -148,6 +148,20 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner
*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*DeprecatedApi* | [**updateApiKey**](doc//DeprecatedApi.md#updateapikey) | **PUT** /api-keys/{id} | Update an API key
*DeprecatedApi* | [**updateAsset**](doc//DeprecatedApi.md#updateasset) | **PUT** /assets/{id} | Update an asset
*DeprecatedApi* | [**updateAssets**](doc//DeprecatedApi.md#updateassets) | **PUT** /assets | Update assets
*DeprecatedApi* | [**updateLibrary**](doc//DeprecatedApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
*DeprecatedApi* | [**updateMemory**](doc//DeprecatedApi.md#updatememory) | **PUT** /memories/{id} | Update a memory
*DeprecatedApi* | [**updateMyPreferences**](doc//DeprecatedApi.md#updatemypreferences) | **PUT** /users/me/preferences | Update my preferences
*DeprecatedApi* | [**updateMyUser**](doc//DeprecatedApi.md#updatemyuser) | **PUT** /users/me | Update current user
*DeprecatedApi* | [**updatePerson**](doc//DeprecatedApi.md#updateperson) | **PUT** /people/{id} | Update person
*DeprecatedApi* | [**updateSession**](doc//DeprecatedApi.md#updatesession) | **PUT** /sessions/{id} | Update a session
*DeprecatedApi* | [**updateStack**](doc//DeprecatedApi.md#updatestack) | **PUT** /stacks/{id} | Update a stack
*DeprecatedApi* | [**updateTag**](doc//DeprecatedApi.md#updatetag) | **PUT** /tags/{id} | Update a tag
*DeprecatedApi* | [**updateUserAdmin**](doc//DeprecatedApi.md#updateuseradmin) | **PUT** /admin/users/{id} | Update a user
*DeprecatedApi* | [**updateUserPreferencesAdmin**](doc//DeprecatedApi.md#updateuserpreferencesadmin) | **PUT** /admin/users/{id}/preferences | Update user preferences
*DeprecatedApi* | [**updateWorkflow**](doc//DeprecatedApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Dismiss a duplicate group
+845
View File
@@ -184,4 +184,849 @@ class DeprecatedApi {
}
return null;
}
/// Update an API key
///
/// Updates the name and permissions of an API key by its ID. The current user must own this API key.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [ApiKeyUpdateDto] apiKeyUpdateDto (required):
Future<Response> updateApiKeyWithHttpInfo(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/api-keys/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = apiKeyUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update an API key
///
/// Updates the name and permissions of an API key by its ID. The current user must own this API key.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [ApiKeyUpdateDto] apiKeyUpdateDto (required):
Future<ApiKeyResponseDto?> updateApiKey(String id, ApiKeyUpdateDto apiKeyUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateApiKeyWithHttpInfo(id, apiKeyUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ApiKeyResponseDto',) as ApiKeyResponseDto;
}
return null;
}
/// Update an asset
///
/// Update information of a specific asset.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateAssetDto] updateAssetDto (required):
Future<Response> updateAssetWithHttpInfo(String id, UpdateAssetDto updateAssetDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateAssetDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update an asset
///
/// Update information of a specific asset.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateAssetDto] updateAssetDto (required):
Future<AssetResponseDto?> updateAsset(String id, UpdateAssetDto updateAssetDto, { Future<void>? abortTrigger, }) async {
final response = await updateAssetWithHttpInfo(id, updateAssetDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetResponseDto',) as AssetResponseDto;
}
return null;
}
/// Update assets
///
/// Updates multiple assets at the same time.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [AssetBulkUpdateDto] assetBulkUpdateDto (required):
Future<Response> updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets';
// ignore: prefer_final_locals
Object? postBody = assetBulkUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update assets
///
/// Updates multiple assets at the same time.
///
/// Parameters:
///
/// * [AssetBulkUpdateDto] assetBulkUpdateDto (required):
Future<void> updateAssets(AssetBulkUpdateDto assetBulkUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Update a library
///
/// Update an existing external library.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateLibraryDto] updateLibraryDto (required):
Future<Response> updateLibraryWithHttpInfo(String id, UpdateLibraryDto updateLibraryDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/libraries/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateLibraryDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a library
///
/// Update an existing external library.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateLibraryDto] updateLibraryDto (required):
Future<LibraryResponseDto?> updateLibrary(String id, UpdateLibraryDto updateLibraryDto, { Future<void>? abortTrigger, }) async {
final response = await updateLibraryWithHttpInfo(id, updateLibraryDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LibraryResponseDto',) as LibraryResponseDto;
}
return null;
}
/// Update a memory
///
/// Update an existing memory by its ID.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [MemoryUpdateDto] memoryUpdateDto (required):
Future<Response> updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/memories/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = memoryUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a memory
///
/// Update an existing memory by its ID.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [MemoryUpdateDto] memoryUpdateDto (required):
Future<MemoryResponseDto?> updateMemory(String id, MemoryUpdateDto memoryUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto;
}
return null;
}
/// Update my preferences
///
/// Update the preferences of the current user.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<Response> updateMyPreferencesWithHttpInfo(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/users/me/preferences';
// ignore: prefer_final_locals
Object? postBody = userPreferencesUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update my preferences
///
/// Update the preferences of the current user.
///
/// Parameters:
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<UserPreferencesResponseDto?> updateMyPreferences(UserPreferencesUpdateDto userPreferencesUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateMyPreferencesWithHttpInfo(userPreferencesUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto;
}
return null;
}
/// Update current user
///
/// Update the current user making the API request.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [UserUpdateMeDto] userUpdateMeDto (required):
Future<Response> updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/users/me';
// ignore: prefer_final_locals
Object? postBody = userUpdateMeDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update current user
///
/// Update the current user making the API request.
///
/// Parameters:
///
/// * [UserUpdateMeDto] userUpdateMeDto (required):
Future<UserAdminResponseDto?> updateMyUser(UserUpdateMeDto userUpdateMeDto, { Future<void>? abortTrigger, }) async {
final response = await updateMyUserWithHttpInfo(userUpdateMeDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
}
return null;
}
/// Update person
///
/// Update an individual person.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [PersonUpdateDto] personUpdateDto (required):
Future<Response> updatePersonWithHttpInfo(String id, PersonUpdateDto personUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/people/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = personUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update person
///
/// Update an individual person.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [PersonUpdateDto] personUpdateDto (required):
Future<PersonResponseDto?> updatePerson(String id, PersonUpdateDto personUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updatePersonWithHttpInfo(id, personUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
}
return null;
}
/// Update a session
///
/// Update a specific session identified by id.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [SessionUpdateDto] sessionUpdateDto (required):
Future<Response> updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/sessions/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = sessionUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a session
///
/// Update a specific session identified by id.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [SessionUpdateDto] sessionUpdateDto (required):
Future<SessionResponseDto?> updateSession(String id, SessionUpdateDto sessionUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateSessionWithHttpInfo(id, sessionUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SessionResponseDto',) as SessionResponseDto;
}
return null;
}
/// Update a stack
///
/// Update an existing stack by its ID.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [StackUpdateDto] stackUpdateDto (required):
Future<Response> updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/stacks/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = stackUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a stack
///
/// Update an existing stack by its ID.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [StackUpdateDto] stackUpdateDto (required):
Future<StackResponseDto?> updateStack(String id, StackUpdateDto stackUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateStackWithHttpInfo(id, stackUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto;
}
return null;
}
/// Update a tag
///
/// Update an existing tag identified by its ID.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [TagUpdateDto] tagUpdateDto (required):
Future<Response> updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/tags/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = tagUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a tag
///
/// Update an existing tag identified by its ID.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [TagUpdateDto] tagUpdateDto (required):
Future<TagResponseDto?> updateTag(String id, TagUpdateDto tagUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateTagWithHttpInfo(id, tagUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TagResponseDto',) as TagResponseDto;
}
return null;
}
/// Update a user
///
/// Update an existing user.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserAdminUpdateDto] userAdminUpdateDto (required):
Future<Response> updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/users/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = userAdminUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a user
///
/// Update an existing user.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserAdminUpdateDto] userAdminUpdateDto (required):
Future<UserAdminResponseDto?> updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
}
return null;
}
/// Update user preferences
///
/// Update the preferences of a specific user.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<Response> updateUserPreferencesAdminWithHttpInfo(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/users/{id}/preferences'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = userPreferencesUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update user preferences
///
/// Update the preferences of a specific user.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserPreferencesUpdateDto] userPreferencesUpdateDto (required):
Future<UserPreferencesResponseDto?> updateUserPreferencesAdmin(String id, UserPreferencesUpdateDto userPreferencesUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateUserPreferencesAdminWithHttpInfo(id, userPreferencesUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserPreferencesResponseDto',) as UserPreferencesResponseDto;
}
return null;
}
/// Update a workflow
///
/// Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [WorkflowUpdateDto] workflowUpdateDto (required):
Future<Response> updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto, { Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = workflowUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Update a workflow
///
/// Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [WorkflowUpdateDto] workflowUpdateDto (required):
Future<WorkflowResponseDto?> updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto, { Future<void>? abortTrigger, }) async {
final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'WorkflowResponseDto',) as WorkflowResponseDto;
}
return null;
}
}
+2 -2
View File
@@ -97,9 +97,9 @@ class ApiClient {
if (nullableHeaderParams != null) {
request.headers.addAll(nullableHeaderParams);
}
if (msgBody is String) {
if (msgBody is String && msgBody.isNotEmpty) {
request.body = msgBody;
} else if (msgBody is List<int>) {
} else if (msgBody is List<int> && msgBody.isNotEmpty) {
request.bodyBytes = msgBody;
} else if (msgBody is Map<String, String>) {
request.bodyFields = msgBody;
@@ -35,16 +35,16 @@ class TimeBucketAssetResponseDto {
});
/// Array of city names extracted from EXIF GPS data
Optional<List<String>?> city;
Optional<List<String?>?> city;
/// Array of country names extracted from EXIF GPS data
Optional<List<String>?> country;
Optional<List<String?>?> country;
/// Array of UTC timestamps when each asset was originally uploaded to Immich
List<String> createdAt;
/// Array of video/gif durations in milliseconds (null for static images)
List<int> duration;
List<int?> duration;
/// Array of file creation timestamps in UTC
List<String> fileCreatedAt;
@@ -62,22 +62,22 @@ class TimeBucketAssetResponseDto {
List<bool> isTrashed;
/// Array of latitude coordinates extracted from EXIF GPS data
Optional<List<num>?> latitude;
Optional<List<num?>?> latitude;
/// Array of live photo video asset IDs (null for non-live photos)
List<String> livePhotoVideoId;
List<String?> livePhotoVideoId;
/// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.
List<num> localOffsetHours;
/// Array of longitude coordinates extracted from EXIF GPS data
Optional<List<num>?> longitude;
Optional<List<num?>?> longitude;
/// Array of owner IDs for each asset
List<String> ownerId;
/// Array of projection types for 360° content (e.g., \"EQUIRECTANGULAR\", \"CUBEFACE\", \"CYLINDRICAL\")
List<String> projectionType;
List<String?> projectionType;
/// Array of aspect ratios (width/height) for each asset
List<num> ratio;
@@ -86,7 +86,7 @@ class TimeBucketAssetResponseDto {
Optional<List<List<String>?>?> stack;
/// Array of BlurHash strings for generating asset previews (base64 encoded)
List<String> thumbhash;
List<String?> thumbhash;
/// Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)
List<AssetVisibility> visibility;
+1 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
OPENAPI_GENERATOR_VERSION=v7.22.0
OPENAPI_GENERATOR_VERSION=v7.23.0
set -euo pipefail
@@ -23,7 +23,6 @@ patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/asset_edit_action_item_dto.dart <./patch/asset_edit_action_item_dto.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/time_bucket_asset_response_dto.dart <./patch/time_bucket_asset_response_dto.dart.patch
# Don't include analysis_options.yaml for the generated openapi files
# so that language servers can properly exclude the mobile/openapi directory
rm ../mobile/openapi/analysis_options.yaml
+127 -28
View File
@@ -1194,6 +1194,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an existing user.",
"operationId": "updateUserAdmin",
"parameters": [
@@ -1243,7 +1244,8 @@
],
"summary": "Update a user",
"tags": [
"Users (admin)"
"Users (admin)",
"Deprecated"
],
"x-immich-admin-only": true,
"x-immich-history": [
@@ -1258,10 +1260,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateUserAdmin"
}
],
"x-immich-permission": "adminUser.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/admin/users/{id}/calendar-heatmap": {
@@ -1416,6 +1423,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update the preferences of a specific user.",
"operationId": "updateUserPreferencesAdmin",
"parameters": [
@@ -1465,7 +1473,8 @@
],
"summary": "Update user preferences",
"tags": [
"Users (admin)"
"Users (admin)",
"Deprecated"
],
"x-immich-admin-only": true,
"x-immich-history": [
@@ -1480,10 +1489,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateUserPreferencesAdmin"
}
],
"x-immich-permission": "adminUser.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/admin/users/{id}/restore": {
@@ -2863,6 +2877,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Updates the name and permissions of an API key by its ID. The current user must own this API key.",
"operationId": "updateApiKey",
"parameters": [
@@ -2912,7 +2927,8 @@
],
"summary": "Update an API key",
"tags": [
"API keys"
"API keys",
"Deprecated"
],
"x-immich-history": [
{
@@ -2926,10 +2942,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateApiKey"
}
],
"x-immich-permission": "apiKey.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/assets": {
@@ -3080,6 +3101,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Updates multiple assets at the same time.",
"operationId": "updateAssets",
"parameters": [],
@@ -3111,7 +3133,8 @@
],
"summary": "Update assets",
"tags": [
"Assets"
"Assets",
"Deprecated"
],
"x-immich-history": [
{
@@ -3125,10 +3148,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateAssets"
}
],
"x-immich-permission": "asset.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/assets/bulk-upload-check": {
@@ -3557,6 +3585,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update information of a specific asset.",
"operationId": "updateAsset",
"parameters": [
@@ -3606,7 +3635,8 @@
],
"summary": "Update an asset",
"tags": [
"Assets"
"Assets",
"Deprecated"
],
"x-immich-history": [
{
@@ -3620,10 +3650,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateAsset"
}
],
"x-immich-permission": "asset.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/assets/{id}/edits": {
@@ -6329,6 +6364,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an existing external library.",
"operationId": "updateLibrary",
"parameters": [
@@ -6378,7 +6414,8 @@
],
"summary": "Update a library",
"tags": [
"Libraries"
"Libraries",
"Deprecated"
],
"x-immich-admin-only": true,
"x-immich-history": [
@@ -6393,10 +6430,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateLibrary"
}
],
"x-immich-permission": "library.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/libraries/{id}/scan": {
@@ -7165,6 +7207,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an existing memory by its ID.",
"operationId": "updateMemory",
"parameters": [
@@ -7214,7 +7257,8 @@
],
"summary": "Update a memory",
"tags": [
"Memories"
"Memories",
"Deprecated"
],
"x-immich-history": [
{
@@ -7228,10 +7272,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateMemory"
}
],
"x-immich-permission": "memory.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/memories/{id}/assets": {
@@ -8711,6 +8760,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an individual person.",
"operationId": "updatePerson",
"parameters": [
@@ -8760,7 +8810,8 @@
],
"summary": "Update person",
"tags": [
"People"
"People",
"Deprecated"
],
"x-immich-history": [
{
@@ -8774,10 +8825,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updatePerson"
}
],
"x-immich-permission": "person.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/people/{id}/merge": {
@@ -11529,6 +11585,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update a specific session identified by id.",
"operationId": "updateSession",
"parameters": [
@@ -11578,7 +11635,8 @@
],
"summary": "Update a session",
"tags": [
"Sessions"
"Sessions",
"Deprecated"
],
"x-immich-history": [
{
@@ -11592,10 +11650,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateSession"
}
],
"x-immich-permission": "session.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/sessions/{id}/lock": {
@@ -12545,6 +12608,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an existing stack by its ID.",
"operationId": "updateStack",
"parameters": [
@@ -12594,7 +12658,8 @@
],
"summary": "Update a stack",
"tags": [
"Stacks"
"Stacks",
"Deprecated"
],
"x-immich-history": [
{
@@ -12608,10 +12673,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateStack"
}
],
"x-immich-permission": "stack.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/stacks/{id}/assets/{assetId}": {
@@ -13648,6 +13718,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update an existing tag identified by its ID.",
"operationId": "updateTag",
"parameters": [
@@ -13697,7 +13768,8 @@
],
"summary": "Update a tag",
"tags": [
"Tags"
"Tags",
"Deprecated"
],
"x-immich-history": [
{
@@ -13711,10 +13783,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateTag"
}
],
"x-immich-permission": "tag.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/tags/{id}/assets": {
@@ -14517,6 +14594,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update the current user making the API request.",
"operationId": "updateMyUser",
"parameters": [],
@@ -14555,7 +14633,8 @@
],
"summary": "Update current user",
"tags": [
"Users"
"Users",
"Deprecated"
],
"x-immich-history": [
{
@@ -14569,10 +14648,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateMyUser"
}
],
"x-immich-permission": "user.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/users/me/calendar-heatmap": {
@@ -15003,6 +15087,7 @@
"x-immich-state": "Stable"
},
"put": {
"deprecated": true,
"description": "Update the preferences of the current user.",
"operationId": "updateMyPreferences",
"parameters": [],
@@ -15041,7 +15126,8 @@
],
"summary": "Update my preferences",
"tags": [
"Users"
"Users",
"Deprecated"
],
"x-immich-history": [
{
@@ -15055,10 +15141,15 @@
{
"version": "v2",
"state": "Stable"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateMyPreferences"
}
],
"x-immich-permission": "userPreference.update",
"x-immich-state": "Stable"
"x-immich-state": "Deprecated"
}
},
"/users/profile-image": {
@@ -15680,6 +15771,7 @@
"x-immich-permission": "workflow.read"
},
"put": {
"deprecated": true,
"description": "Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.",
"operationId": "updateWorkflow",
"parameters": [
@@ -15729,15 +15821,22 @@
],
"summary": "Update a workflow",
"tags": [
"Workflows"
"Workflows",
"Deprecated"
],
"x-immich-history": [
{
"version": "v3.0.0",
"state": "Added"
},
{
"version": "v3",
"state": "Deprecated",
"replacementId": "updateWorkflow"
}
],
"x-immich-permission": "workflow.update"
"x-immich-permission": "workflow.update",
"x-immich-state": "Deprecated"
}
},
"/workflows/{id}/share": {
+1 -1
View File
@@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.22.0"
"version": "7.23.0"
}
}
+7 -73
View File
@@ -1,96 +1,30 @@
@@ -13,7 +13,7 @@
class ApiClient {
ApiClient({this.basePath = '/api', this.authentication,});
- final String basePath;
+ String basePath;
final Authentication? authentication;
var _client = Client();
@@ -44,8 +44,9 @@
Object? body,
Map<String, String> headerParams,
Map<String, String> formParams,
- String? contentType,
- ) async {
+ String? contentType, {
+ Future<void>? abortTrigger,
+ }) async {
await authentication?.applyToParams(queryParams, headerParams);
headerParams.addAll(_defaultHeaderMap);
@@ -63,7 +64,7 @@
body is MultipartFile && (contentType == null ||
!contentType.toLowerCase().startsWith('multipart/form-data'))
) {
- final request = StreamedRequest(method, uri);
+ final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger);
request.headers.addAll(headerParams);
request.contentLength = body.length;
body.finalize().listen(
@@ -78,7 +79,7 @@
}
if (body is MultipartRequest) {
- final request = MultipartRequest(method, uri);
+ final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger);
request.fields.addAll(body.fields);
request.files.addAll(body.files);
request.headers.addAll(body.headers);
@@ -92,14 +93,19 @@
: await serializeAsync(body);
final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
- switch(method) {
- case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
- case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
- case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
- case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
- case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
- case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
+ final request = AbortableRequest(method, uri, abortTrigger: abortTrigger);
+ if (nullableHeaderParams != null) {
+ request.headers.addAll(nullableHeaderParams);
}
+ if (msgBody is String) {
+ request.body = msgBody;
+ } else if (msgBody is List<int>) {
+ request.bodyBytes = msgBody;
+ } else if (msgBody is Map<String, String>) {
+ request.bodyFields = msgBody;
+ }
+ final response = await _client.send(request);
+ return Response.fromStream(response);
} on SocketException catch (error, trace) {
throw ApiException.withInner(
HttpStatus.badRequest,
@@ -136,26 +146,21 @@
trace,
);
}
-
- throw ApiException(
- HttpStatus.badRequest,
- 'Invalid HTTP operation: $method $path',
- );
@@ -143,19 +143,19 @@
);
}
- Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
+ Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package
deserialize(value, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
- dynamic deserialize(String value, String targetType, {bool growable = false,}) {
+ Future<dynamic> deserialize(String value, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? value
- : fromJson(json.decode(value), targetType, growable: growable);
+ : fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable);
}
// ignore: deprecated_member_use_from_same_package
@@ -1,9 +0,0 @@
@@ -83,7 +83,7 @@
List<num> ratio;
/// Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)
- Optional<List<List<String>>?> stack;
+ Optional<List<List<String>?>?> stack;
/// Array of BlurHash strings for generating asset previews (base64 encoded)
List<String> thumbhash;
+3 -24
View File
@@ -1,11 +1,8 @@
--- api.mustache
+++ api.mustache.modified
@@ -49,9 +49,9 @@
///
{{/-last}}
@@ -51,7 +51,7 @@
{{/allParams}}
- Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
+ Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? abortTrigger, }) async {
Future<Response> {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
// ignore: prefer_const_declarations
- final path = r'{{{path}}}'{{#pathParams}}
+ final apiPath = r'{{{path}}}'{{#pathParams}}
@@ -21,7 +18,7 @@
{{#formParams}}
{{^isFile}}
if ({{{paramName}}} != null) {
@@ -121,13 +121,14 @@
@@ -121,7 +121,7 @@
{{/isMultipart}}
return apiClient.invokeAPI(
@@ -30,21 +27,3 @@
'{{{httpMethod}}}',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
+ abortTrigger: abortTrigger,
);
}
@@ -161,8 +162,8 @@
///
{{/-last}}
{{/allParams}}
- Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async {
- final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}});
+ Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future<void>? abortTrigger, }) async {
+ final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -26,155 +26,3 @@
return {{{classname}}}(
{{#vars}}
{{#isDateTime}}
@@ -195,48 +181,98 @@
{{#complexType}}
{{#isArray}}
{{#items.isArray}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is List
+ ? (json[r'{{{baseName}}}'] as List).map((e) =>
+ {{#items.complexType}}
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
+ {{/items.complexType}}
+ {{^items.complexType}}
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
+ {{/items.complexType}}
+ ).toList()
+ : {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}}) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] is List
? (json[r'{{{baseName}}}'] as List).map((e) =>
{{#items.complexType}}
- {{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}}
{{/items.complexType}}
{{^items.complexType}}
- e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>()
+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)
{{/items.complexType}}
).toList()
: {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}},
+ {{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{^items.isArray}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
+ {{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
{{#items.isArray}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null
+ {{#items.complexType}}
+ : {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
+ {{/items.complexType}}
+ {{^items.complexType}}
+ : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)))) : const Optional.absent(),
+ {{/items.complexType}}
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
- {{#items.complexType}}
+ {{#items.complexType}}
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
- {{/items.complexType}}
- {{^items.complexType}}
- : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (v as List).cast<{{items.items.dataType}}>())),
- {{/items.complexType}}
+ {{/items.complexType}}
+ {{^items.complexType}}
+ : (json[r'{{{baseName}}}'] as Map<String, dynamic>).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false))),
+ {{/items.complexType}}
+ {{/vendorExtensions.x-is-optional}}
{{/items.isArray}}
{{^items.isArray}}
{{#items.isMap}}
{{#items.complexType}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{items.complexType}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: {{items.complexType}}.mapFromJson(json[r'{{{baseName}}}']),
+ {{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{^items.complexType}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, dynamic>(json, r'{{{baseName}}}')) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, dynamic>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
+ {{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{/items.isMap}}
{{^items.isMap}}
{{#items.complexType}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}']),
+ {{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{^items.complexType}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, {{items.dataType}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
+ {{/vendorExtensions.x-is-optional}}
{{/items.complexType}}
{{/items.isMap}}
{{/items.isArray}}
@@ -259,23 +295,45 @@
{{^complexType}}
{{#isArray}}
{{#isEnum}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}},
+ {{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{^isEnum}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is Iterable
+ ? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
+ : {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: json[r'{{{baseName}}}'] is Iterable
? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}
: {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}},
+ {{/vendorExtensions.x-is-optional}}
{{/isEnum}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}')) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: mapCastOfType<String, {{{items.datatype}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
+ {{/vendorExtensions.x-is-optional}}
{{/isMap}}
{{^isMap}}
{{#isNumber}}
+ {{#vendorExtensions.x-is-optional}}
+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(),
+ {{/vendorExtensions.x-is-optional}}
+ {{^vendorExtensions.x-is-optional}}
{{{name}}}: {{#isNullable}}json[r'{{{baseName}}}'] == null
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
+ {{/vendorExtensions.x-is-optional}}
{{/isNumber}}
{{^isNumber}}
{{#vendorExtensions.x-original-is-integer}}
+1 -1
View File
@@ -7,7 +7,7 @@
"format": "prettier --cache --check i18n/",
"format:fix": "prettier --cache --write --list-different i18n"
},
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"packageManager": "pnpm@11.4.0",
"engines": {
"pnpm": ">=10.0.0"
},
+5 -2
View File
@@ -1,6 +1,9 @@
FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25
WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./
COPY ./packages ./packages/
WORKDIR /usr/src/app/packages/e2e-auth-server
RUN corepack enable
ADD package.json *.ts ./
RUN pnpm install
RUN pnpm install --frozen-lockfile
EXPOSE 2286
CMD ["pnpm", "run", "start"]
+1 -1
View File
@@ -13,5 +13,5 @@
"oidc-provider": "^9.0.0",
"tsx": "^4.20.6"
},
"packageManager": "pnpm@10.33.4"
"packageManager": "pnpm@11.4.0"
}
+1 -1
View File
@@ -24,7 +24,7 @@
"keywords": [],
"author": "",
"license": "GNU Affero General Public License version 3",
"packageManager": "pnpm@10.33.4",
"packageManager": "pnpm@11.4.0",
"devDependencies": {
"@extism/js-pdk": "^1.1.1",
"@immich/sdk": "workspace:*",
-1
View File
@@ -8,7 +8,6 @@ settings:
overrides:
canvas: 3.2.3
sharp: ^0.34.5
webpackbar: ^7.0.0
packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k=
+19 -22
View File
@@ -8,31 +8,28 @@ packages:
- web
- .github
- packages/*
ignoredBuiltDependencies:
- '@nestjs/core'
- '@parcel/watcher'
- '@scarf/scarf'
- '@swc/core'
- canvas
- core-js
- core-js-pure
- cpu-features
- es5-ext
- esbuild
- msgpackr-extract
- postman-code-generators
- protobufjs
- ssh2
- utimes
onlyBuiltDependencies:
- sharp
- '@tailwindcss/oxide'
- bcrypt
allowBuilds:
'@nestjs/core': false
'@parcel/watcher': false
'@scarf/scarf': false
'@swc/core': false
bcrypt: true
canvas: false
core-js: false
cpu-features: false
es5-ext: false
esbuild: false
msgpackr-extract: false
protobufjs: false
sharp: true
ssh2: false
utimes: false
'@tailwindcss/oxide': true
core-js-pure: false
postman-code-generators: false
overrides:
canvas: 3.2.3
sharp: ^0.34.5
# pending docusaurus 3.10.1
webpackbar: ^7.0.0
packageExtensions:
nestjs-kysely:
dependencies:
+4 -4
View File
@@ -20,8 +20,8 @@ RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich --frozen-lockfile build && \
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich build && \
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --prod --no-optional deploy /output/server-pruned
FROM builder AS web
@@ -37,7 +37,7 @@ RUN --mount=type=cache,id=pnpm-web,target=/buildcache/pnpm-store \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install && \
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter immich-web install --frozen-lockfile --force && \
pnpm --filter @immich/sdk --filter immich-web build
FROM builder AS cli
@@ -48,7 +48,7 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install && \
pnpm --filter @immich/sdk --filter @immich/cli install --frozen-lockfile && \
pnpm --filter @immich/sdk --filter @immich/cli build && \
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
+18 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { ApiKeyCreateDto, ApiKeyCreateResponseDto, ApiKeyResponseDto, ApiKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -62,7 +62,11 @@ export class ApiKeyController {
@Endpoint({
summary: 'Update an API key',
description: 'Updates the name and permissions of an API key by its ID. The current user must own this API key.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateApiKey' }),
})
updateApiKey(
@Auth() auth: AuthDto,
@@ -72,6 +76,17 @@ export class ApiKeyController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.ApiKeyUpdate })
updateApiKeyV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: ApiKeyUpdateDto,
): Promise<ApiKeyResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.ApiKeyDelete })
@HttpCode(HttpStatus.NO_CONTENT)
+31 -4
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import {
@@ -59,12 +59,24 @@ export class AssetController {
@Endpoint({
summary: 'Update assets',
description: 'Updates multiple assets at the same time.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateAssets' }),
})
updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto);
}
@Patch()
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.AssetUpdate })
@HttpCode(HttpStatus.NO_CONTENT)
updateAssetsV3(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(auth, dto);
}
@Delete()
@Authenticated({ permission: Permission.AssetDelete })
@HttpCode(HttpStatus.NO_CONTENT)
@@ -131,7 +143,11 @@ export class AssetController {
@Endpoint({
summary: 'Update an asset',
description: 'Update information of a specific asset.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateAsset' }),
})
updateAsset(
@Auth() auth: AuthDto,
@@ -141,6 +157,17 @@ export class AssetController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.AssetUpdate })
updateAssetV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateAssetDto,
): Promise<AssetResponseDto> {
return this.service.update(auth, id, dto);
}
@Get(':id/metadata')
@Authenticated({ permission: Permission.AssetRead })
@Endpoint({
+14 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import {
CreateLibraryDto,
@@ -57,12 +57,23 @@ export class LibraryController {
@Endpoint({
summary: 'Update a library',
description: 'Update an existing external library.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateLibrary' }),
})
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.LibraryUpdate, admin: true })
updateLibraryV3(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.LibraryDelete, admin: true })
@HttpCode(HttpStatus.NO_CONTENT)
+18 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -71,7 +71,11 @@ export class MemoryController {
@Endpoint({
summary: 'Update a memory',
description: 'Update an existing memory by its ID.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateMemory' }),
})
updateMemory(
@Auth() auth: AuthDto,
@@ -81,6 +85,17 @@ export class MemoryController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.MemoryUpdate })
updateMemoryV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: MemoryUpdateDto,
): Promise<MemoryResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.MemoryDelete })
@HttpCode(HttpStatus.NO_CONTENT)
+18 -2
View File
@@ -7,12 +7,13 @@ import {
HttpStatus,
Next,
Param,
Patch,
Post,
Put,
Query,
Res,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
@@ -106,7 +107,11 @@ export class PersonController {
@Endpoint({
summary: 'Update person',
description: 'Update an individual person.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updatePerson' }),
})
updatePerson(
@Auth() auth: AuthDto,
@@ -116,6 +121,17 @@ export class PersonController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.PersonUpdate })
updatePersonV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: PersonUpdateDto,
): Promise<PersonResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.PersonDelete })
@HttpCode(HttpStatus.NO_CONTENT)
+18 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto';
@@ -52,7 +52,11 @@ export class SessionController {
@Endpoint({
summary: 'Update a session',
description: 'Update a specific session identified by id.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateSession' }),
})
updateSession(
@Auth() auth: AuthDto,
@@ -62,6 +66,17 @@ export class SessionController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.SessionUpdate })
updateSessionV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: SessionUpdateDto,
): Promise<SessionResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.SessionDelete })
@HttpCode(HttpStatus.NO_CONTENT)
+18 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -65,7 +65,11 @@ export class StackController {
@Endpoint({
summary: 'Update a stack',
description: 'Update an existing stack by its ID.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateStack' }),
})
updateStack(
@Auth() auth: AuthDto,
@@ -75,6 +79,17 @@ export class StackController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.StackUpdate })
updateStackV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: StackUpdateDto,
): Promise<StackResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.StackDelete })
@HttpCode(HttpStatus.NO_CONTENT)
+14 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -81,12 +81,23 @@ export class TagController {
@Endpoint({
summary: 'Update a tag',
description: 'Update an existing tag identified by its ID.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v3', { replacementId: 'updateTag' }),
})
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.TagUpdate })
updateTagV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: TagUpdateDto,
): Promise<TagResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.TagDelete })
@HttpCode(HttpStatus.NO_CONTENT)
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -61,7 +61,11 @@ export class UserAdminController {
@Endpoint({
summary: 'Update a user',
description: 'Update an existing user.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateUserAdmin' }),
})
updateUserAdmin(
@Auth() auth: AuthDto,
@@ -71,6 +75,17 @@ export class UserAdminController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
updateUserAdminV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UserAdminUpdateDto,
): Promise<UserAdminResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@Endpoint({
@@ -143,7 +158,11 @@ export class UserAdminController {
@Endpoint({
summary: 'Update user preferences',
description: 'Update the preferences of a specific user.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateUserPreferencesAdmin' }),
})
updateUserPreferencesAdmin(
@Auth() auth: AuthDto,
@@ -153,6 +172,17 @@ export class UserAdminController {
return this.service.updatePreferences(auth, id, dto);
}
@Patch(':id/preferences')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
updateUserPreferencesAdminV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UserPreferencesUpdateDto,
): Promise<UserPreferencesResponseDto> {
return this.service.updatePreferences(auth, id, dto);
}
@Post(':id/restore')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@HttpCode(HttpStatus.OK)
+29 -3
View File
@@ -7,6 +7,7 @@ import {
HttpStatus,
Next,
Param,
Patch,
Post,
Put,
Query,
@@ -14,7 +15,7 @@ import {
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiConsumes, ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -78,12 +79,23 @@ export class UserController {
@Endpoint({
summary: 'Update current user',
description: 'Update the current user making the API request.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateMyUser' }),
})
updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
return this.service.updateMe(auth, dto);
}
@Patch('me')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.UserUpdate })
updateMyUserV3(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
return this.service.updateMe(auth, dto);
}
@Get('me/preferences')
@Authenticated({ permission: Permission.UserPreferenceRead })
@Endpoint({
@@ -100,7 +112,11 @@ export class UserController {
@Endpoint({
summary: 'Update my preferences',
description: 'Update the preferences of the current user.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3', { replacementId: 'updateMyPreferences' }),
})
updateMyPreferences(
@Auth() auth: AuthDto,
@@ -109,6 +125,16 @@ export class UserController {
return this.service.updateMyPreferences(auth, dto);
}
@Patch('me/preferences')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.UserPreferenceUpdate })
updateMyPreferencesV3(
@Auth() auth: AuthDto,
@Body() dto: UserPreferencesUpdateDto,
): Promise<UserPreferencesResponseDto> {
return this.service.updateMyPreferences(auth, dto);
}
@Get('me/license')
@Authenticated({ permission: Permission.UserLicenseRead })
@Endpoint({
@@ -95,15 +95,15 @@ describe(WorkflowController.name, () => {
});
});
describe('PUT /workflows/:id', () => {
describe('PATCH /workflows/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/workflows/${factory.uuid()}`).send({});
await request(ctx.getHttpServer()).patch(`/workflows/${factory.uuid()}`).send({});
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.put(`/workflows/invalid`)
.patch(`/workflows/invalid`)
.set('Authorization', `Bearer token`)
.send({});
expect(status).toBe(400);
+14 -3
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import {
@@ -81,7 +81,7 @@ export class WorkflowController {
summary: 'Update a workflow',
description:
'Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.',
history: HistoryBuilder.v3(),
history: new HistoryBuilder().added('v3.0.0').deprecated('v3', { replacementId: 'updateWorkflow' }),
})
updateWorkflow(
@Auth() auth: AuthDto,
@@ -91,6 +91,17 @@ export class WorkflowController {
return this.service.update(auth, id, dto);
}
@Patch(':id')
@ApiExcludeEndpoint()
@Authenticated({ permission: Permission.WorkflowUpdate })
updateWorkflowV3(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: WorkflowUpdateDto,
): Promise<WorkflowResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.WorkflowDelete })
@HttpCode(HttpStatus.NO_CONTENT)
@@ -84,7 +84,7 @@ export class MetadataRepository {
inferTimezoneFromDatestamps: true,
inferTimezoneFromTimeStamp: true,
useMWG: true,
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength', 'FileSize'],
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength', 'FileSize', 'Rotation'],
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
geoTz: (lat, lon) => geotz.find(lat, lon)[0],
geolocation: true,
+19 -11
View File
@@ -348,17 +348,25 @@ describe(AlbumService.name, () => {
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId, 'asset-2']), false);
});
it('should throw an error if the userId is the ownerId', async () => {
const album = AlbumFactory.create();
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
mocks.user.get.mockResolvedValue(owner);
await expect(
sut.create(AuthFactory.create(owner), {
albumName: 'Empty album',
albumUsers: [{ userId: owner.id, role: AlbumUserRole.Editor }],
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.album.create).not.toHaveBeenCalled();
it('should deduplicate owner from albumUsers on create', async () => {
const auth = AuthFactory.create();
const album = AlbumFactory.from().build();
mocks.album.create.mockResolvedValue(getForAlbum(album));
mocks.user.getMetadata.mockResolvedValue([]);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
await sut.create(auth, {
albumName: 'Empty album',
albumUsers: [{ userId: auth.user.id, role: AlbumUserRole.Editor }],
});
expect(mocks.user.get).not.toHaveBeenCalled();
expect(mocks.album.create).toHaveBeenCalledWith(
expect.objectContaining({ albumName: 'Empty album' }),
[],
[{ userId: auth.user.id, role: AlbumUserRole.Owner }],
auth.user.id,
);
});
});
+1 -5
View File
@@ -98,7 +98,7 @@ export class AlbumService extends BaseService {
}
async create(auth: AuthDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
const albumUsers = dto.albumUsers || [];
const albumUsers = (dto.albumUsers || []).filter(({ userId }) => userId !== auth.user.id);
for (const { userId } of albumUsers) {
const exists = await this.userRepository.get(userId, {});
@@ -106,10 +106,6 @@ export class AlbumService extends BaseService {
this.logger.debug('Album creation failed: user not found');
throw new BadRequestException('Invalid user');
}
if (userId == auth.user.id) {
throw new BadRequestException('Cannot share album with owner');
}
}
const allowedAssetIdsSet = await this.checkAccess({
+31
View File
@@ -616,6 +616,17 @@ export class MetadataService extends BaseService {
// never use duration from sidecar
delete sidecarTags?.Duration;
// don't use Exif Orientation for HEIF based images, it's usually missing or invalid.
// prefer irot (ExifTool QuickTime:Rotation) mapped to ExifOrientation.
if (mimeTypes.isHeifImage(asset.originalPath)) {
const orientation = this.getHeifOrientation(mediaTags);
if (orientation === null) {
delete mediaTags.Orientation;
} else {
mediaTags.Orientation = orientation;
}
}
return {
tags: { ...mediaTags, ...videoResult?.tags, ...sidecarTags },
audio: videoResult?.audio,
@@ -1110,4 +1121,24 @@ export class MetadataService extends BaseService {
return { tags, audio, video, packets, format };
}
private getHeifOrientation(exifTags: ImmichTags): ExifOrientation | null {
// https://exiftool.org/TagNames/QuickTime.html#ItemPropCont
const rotation = typeof exifTags.Rotation === 'number' ? exifTags.Rotation : undefined;
switch (rotation) {
case 0: {
return ExifOrientation.Horizontal;
}
case 1: {
return ExifOrientation.Rotate270CW;
}
case 2: {
return ExifOrientation.Rotate180;
}
case 3: {
return ExifOrientation.Rotate90CW;
}
}
return null;
}
}
+6
View File
@@ -74,6 +74,11 @@ const possiblyAnimatedImage: Record<string, string[]> = Object.fromEntries(
Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)),
);
const heifImageExtensions = new Set(['.avif', '.heic', '.heif', '.hif']);
const heifImage: Record<string, string[]> = Object.fromEntries(
Object.entries(image).filter(([key]) => heifImageExtensions.has(key)),
);
const extensionOverrides: Record<string, string> = {
'image/jpeg': '.jpg',
};
@@ -147,6 +152,7 @@ export const mimeTypes = {
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
isImage: (filename: string) => isType(filename, image),
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
isHeifImage: (filename: string) => isType(filename, heifImage),
isPossiblyAnimatedImage: (filename: string) => isType(filename, possiblyAnimatedImage),
isProfile: (filename: string) => isType(filename, profile),
isSidecar: (filename: string) => isType(filename, sidecar),
@@ -33,6 +33,7 @@
import UserAvatar from '../shared-components/UserAvatar.svelte';
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
import { faceManager } from '$lib/stores/face.svelte';
interface Props {
asset: AssetResponseDto;
@@ -97,6 +98,8 @@
const handleRefreshPeople = async () => {
asset = await getAssetInfo({ id: asset.id });
assetViewerManager.closeEditFacesPanel();
faceManager.clear();
await faceManager.getAssetFaces(asset.id);
};
const getAssetFolderHref = (asset: AssetResponseDto) => {
@@ -3,6 +3,7 @@
import ImageThumbnail from '$lib/components/assets/thumbnail/ImageThumbnail.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import FaceCreateTagModal from '$lib/modals/CreateFaceModal.svelte';
import { faceManager } from '$lib/stores/face.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { getNaturalSize, scaleToFit } from '$lib/utils/container-utils';
import { handleError } from '$lib/utils/handle-error';
@@ -326,6 +327,7 @@
});
await assetViewerManager.setAssetId(assetId);
faceManager.clear();
} catch (error) {
handleError(error, 'Error tagging face');
} finally {
+1
View File
@@ -67,6 +67,7 @@ class FaceManager {
clear() {
this.#cleared = true;
assetCacheManager.clearFaceCache();
this.#data = [];
}
}