Compare commits

..

1 Commits

Author SHA1 Message Date
timonrieger fcd23ee043 refactor: use zod codec for response DTO serialization 2026-04-23 14:21:40 +02:00
156 changed files with 1925 additions and 2220 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
terragrunt = "1.0.2" terragrunt = "1.0.1"
opentofu = "1.11.6" opentofu = "1.11.6"
[tasks."tg:fmt"] [tasks."tg:fmt"]
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3 image: prom/prometheus@sha256:5550dc63da361dc30f6fe02ac0e4dfc736ededfef3c8d12a634db04a67824d78
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
+1 -1
View File
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
### Cons ### Cons
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022. - The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use. - Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
- Tailscale needs to be installed and running on both server-side and client-side. - Tailscale needs to be installed and running on both server-side and client-side.
## Option 3: Reverse Proxy ## Option 3: Reverse Proxy
+39
View File
@@ -2,43 +2,82 @@ import { expect } from 'vitest';
export const errorDto = { export const errorDto = {
unauthorized: { unauthorized: {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required', message: 'Authentication required',
correlationId: expect.any(String),
}, },
unauthorizedWithMessage: (message: string) => ({ unauthorizedWithMessage: (message: string) => ({
error: 'Unauthorized',
statusCode: 401,
message, message,
correlationId: expect.any(String),
}), }),
forbidden: { forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String), message: expect.any(String),
correlationId: expect.any(String),
}, },
missingPermission: (permission: string) => ({ missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`, message: `Missing required permission: ${permission}`,
correlationId: expect.any(String),
}), }),
wrongPassword: { wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password', message: 'Wrong password',
correlationId: expect.any(String),
}, },
invalidToken: { invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token', message: 'Invalid user token',
correlationId: expect.any(String),
}, },
invalidShareKey: { invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key', message: 'Invalid share key',
correlationId: expect.any(String),
}, },
passwordRequired: { passwordRequired: {
error: 'Unauthorized',
statusCode: 401,
message: 'Password required', message: 'Password required',
correlationId: expect.any(String),
}, },
badRequest: (message: any = null) => ({ badRequest: (message: any = null) => ({
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(), message: message ?? expect.anything(),
correlationId: expect.any(String),
}), }),
noPermission: { noPermission: {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'), message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
}, },
incorrectLogin: { incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password', message: 'Incorrect email or password',
correlationId: expect.any(String),
}, },
alreadyHasAdmin: { alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin', message: 'The server already has an admin',
correlationId: expect.any(String),
}, },
invalidEmail: { invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'], message: ['email must be an email'],
correlationId: expect.any(String),
}, },
}; };
+4 -1
View File
@@ -332,7 +332,9 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ expect(body).toMatchObject({
error: 'Internal Server Error',
message: 'Failed to finish oauth', message: 'Failed to finish oauth',
statusCode: 500,
}); });
}); });
@@ -493,10 +495,11 @@ describe(`/oauth`, () => {
}); });
it('should reject OAuth discovery over HTTP', async () => { it('should reject OAuth discovery over HTTP', async () => {
const { status } = await request(app) const { status, body } = await request(app)
.post('/oauth/authorize') .post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ statusCode: 500 });
}); });
}); });
}); });
+4 -4
View File
@@ -48,14 +48,14 @@ FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-core-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-opencl-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb && \ wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/intel-opencl-icd_26.05.37020.3-0_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \ wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file # TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
wget -nv https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb && \ wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \
dpkg -i *.deb && \ dpkg -i *.deb && \
rm *.deb && \ rm *.deb && \
apt-get remove wget -yqq && \ apt-get remove wget -yqq && \
+1 -4
View File
@@ -183,10 +183,7 @@ async def predict(
text: str | None = Form(default=None), text: str | None = Form(default=None),
) -> Any: ) -> Any:
if image is not None: if image is not None:
decoded = await run(lambda: decode_pil(image)) inputs: Image | str = await run(lambda: decode_pil(image))
if decoded.width == 0 or decoded.height == 0:
raise HTTPException(400, "Image has zero width or height")
inputs: Image | str = decoded
elif text is not None: elif text is not None:
inputs = text inputs = text
else: else:
+2 -2
View File
@@ -9,12 +9,12 @@ dependencies = [
"aiocache>=0.12.1,<1.0", "aiocache>=0.12.1,<1.0",
"fastapi>=0.95.2,<1.0", "fastapi>=0.95.2,<1.0",
"gunicorn>=21.1.0", "gunicorn>=21.1.0",
"huggingface-hub>=1.0,<2.0", "huggingface-hub>=0.20.1,<1.0",
"insightface>=0.7.3,<1.0", "insightface>=0.7.3,<1.0",
"numpy<2.4.0", "numpy<2.4.0",
"opencv-python-headless>=4.7.0.72,<5.0", "opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5", "orjson>=3.9.5",
"pillow>=12.2,<13", "pillow>=12.2,<12.3",
"pydantic>=2.0.0,<3", "pydantic>=2.0.0,<3",
"pydantic-settings>=2.5.2,<3", "pydantic-settings>=2.5.2,<3",
"python-multipart>=0.0.6,<1.0", "python-multipart>=0.0.6,<1.0",
-13
View File
@@ -1198,19 +1198,6 @@ class TestLoad:
mock_model.model_format = ModelFormat.ONNX mock_model.model_format = ModelFormat.ONNX
@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)])
def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None:
with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)):
response = deployed_app.post(
"http://localhost:3003/predict",
data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
files={"image": b"fake image bytes"},
)
assert response.status_code == 400
assert "zero" in response.json()["detail"].lower()
def test_root_endpoint(deployed_app: TestClient) -> None: def test_root_endpoint(deployed_app: TestClient) -> None:
response = deployed_app.get("http://localhost:3003") response = deployed_app.get("http://localhost:3003")
+3 -3
View File
@@ -15,9 +15,9 @@ config_roots = [
[tools] [tools]
node = "24.15.0" node = "24.15.0"
flutter = "3.41.7" flutter = "3.41.6"
pnpm = "10.33.1" pnpm = "10.33.0"
terragrunt = "1.0.2" terragrunt = "1.0.1"
opentofu = "1.11.6" opentofu = "1.11.6"
java = "21.0.2" java = "21.0.2"
+1 -1
View File
@@ -1,5 +1,5 @@
app_identifier "app.alextran.immich" # The bundle identifier of your app app_identifier "app.alextran.immich" # The bundle identifier of your app
apple_id "altran@futo.org" # Your Apple email address apple_id "alex.tran1502@gmail.com" # Your Apple email address
# For more information about the Appfile, see: # For more information about the Appfile, see:
+41 -41
View File
@@ -17,11 +17,10 @@ default_platform(:ios)
platform :ios do platform :ios do
# Constants # Constants
TEAM_ID = "2W7AC6T8T5" TEAM_ID = "2F67MQ8R79"
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})" CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich" BASE_BUNDLE_ID = "app.alextran.immich"
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
# Helper method to get App Store Connect API key # Helper method to get App Store Connect API key
def get_api_key def get_api_key
app_store_connect_api_key( app_store_connect_api_key(
@@ -45,45 +44,47 @@ def get_version_from_pubspec
end end
# Helper method to configure code signing for all targets # Helper method to configure code signing for all targets
def configure_code_signing(base_bundle_id:, profile_name_main:, profile_name_share:, profile_name_widget:) def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:)
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
# Runner (main app) # Runner (main app)
update_code_signing_settings( update_code_signing_settings(
use_automatic_signing: false, use_automatic_signing: false,
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: base_bundle_id, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}",
profile_name: profile_name_main, profile_name: profile_name_main,
targets: ["Runner"] targets: ["Runner"]
) )
# ShareExtension # ShareExtension
update_code_signing_settings( update_code_signing_settings(
use_automatic_signing: false, use_automatic_signing: false,
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: "#{base_bundle_id}.ShareExtension", bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension",
profile_name: profile_name_share, profile_name: profile_name_share,
targets: ["ShareExtension"] targets: ["ShareExtension"]
) )
# WidgetExtension # WidgetExtension
update_code_signing_settings( update_code_signing_settings(
use_automatic_signing: false, use_automatic_signing: false,
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: "#{base_bundle_id}.Widget", bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget",
profile_name: profile_name_widget, profile_name: profile_name_widget,
targets: ["WidgetExtension"] targets: ["WidgetExtension"]
) )
end end
# Helper method to build and upload to TestFlight # Helper method to build and upload to TestFlight
def build_and_upload( def build_and_upload(
api_key:, api_key:,
base_bundle_id:, bundle_id_suffix: "",
configuration: "Release", configuration: "Release",
distribute_external: true, distribute_external: true,
version_number: nil, version_number: nil,
@@ -91,8 +92,9 @@ end
profile_name_share:, profile_name_share:,
profile_name_widget: profile_name_widget:
) )
app_identifier = base_bundle_id bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}"
# Set version number if provided # Set version number if provided
if version_number if version_number
increment_version_number(version_number: version_number) increment_version_number(version_number: version_number)
@@ -136,31 +138,31 @@ end
desc "iOS Development Build to TestFlight (requires separate bundle ID)" desc "iOS Development Build to TestFlight (requires separate bundle ID)"
lane :gha_testflight_dev do lane :gha_testflight_dev do
api_key = get_api_key api_key = get_api_key
# Download and install provisioning profiles from App Store Connect # Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain # Certificate is imported by GHA workflow into build.keychain
# Capture profile names after each sigh call # Capture profile names after each sigh call
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME] main_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME] share_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME] widget_profile_name = lane_context[SharedValues::SIGH_NAME]
# Configure code signing for dev bundle IDs using the downloaded profile names # Configure code signing for dev bundle IDs using the downloaded profile names
configure_code_signing( configure_code_signing(
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
) )
# Build and upload # Build and upload
build_and_upload( build_and_upload(
api_key: api_key, api_key: api_key,
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
configuration: "Profile", configuration: "Profile",
distribute_external: false, distribute_external: false,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
@@ -187,7 +189,6 @@ end
# Configure code signing for production bundle IDs # Configure code signing for production bundle IDs
configure_code_signing( configure_code_signing(
base_bundle_id: BASE_BUNDLE_ID,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
@@ -196,7 +197,6 @@ end
# Build and upload with version number # Build and upload with version number
build_and_upload( build_and_upload(
api_key: api_key, api_key: api_key,
base_bundle_id: BASE_BUNDLE_ID,
version_number: get_version_from_pubspec, version_number: get_version_from_pubspec,
distribute_external: false, distribute_external: false,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
@@ -243,30 +243,30 @@ end
desc "iOS Build Only (no TestFlight upload)" desc "iOS Build Only (no TestFlight upload)"
lane :gha_build_only do lane :gha_build_only do
# Use the same build process as the dev TestFlight lane, just skip the upload # Use the same build process as production, just skip the upload
# This ensures PR builds validate the same way as dev TestFlight builds # This ensures PR builds validate the same way as production builds
api_key = get_api_key api_key = get_api_key
# Download and install provisioning profiles from App Store Connect # Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain # Certificate is imported by GHA workflow into build.keychain
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME] main_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME] share_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME] widget_profile_name = lane_context[SharedValues::SIGH_NAME]
# Configure code signing for dev bundle IDs # Configure code signing for dev bundle IDs
configure_code_signing( configure_code_signing(
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
) )
# Build the app (same as gha_testflight_dev but without upload) # Build the app (same as gha_testflight_dev but without upload)
build_app( build_app(
scheme: "Runner", scheme: "Runner",
@@ -277,9 +277,9 @@ end
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
export_options: { export_options: {
provisioningProfiles: { provisioningProfiles: {
DEV_BUNDLE_ID => main_profile_name, "#{BASE_BUNDLE_ID}.development" => main_profile_name,
"#{DEV_BUNDLE_ID}.ShareExtension" => share_profile_name, "#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name,
"#{DEV_BUNDLE_ID}.Widget" => widget_profile_name "#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name
}, },
signingStyle: "manual", signingStyle: "manual",
signingCertificate: CODE_SIGN_IDENTITY signingCertificate: CODE_SIGN_IDENTITY
@@ -3,7 +3,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -192,22 +191,17 @@ class SyncStreamService {
case SyncEntityType.assetV1: case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
await _runWithManageMediaPermission( if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
logContext: "Trashed Assets", final hasPermission = await _localFilesManager.hasManageMediaPermission();
action: () async { if (hasPermission) {
await _handleRemoteDeleted(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id)); await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
await _applyRemoteRestoreToLocal(); await _applyRemoteRestoreToLocal();
}, } else {
); _logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
}
}
return; return;
case SyncEntityType.assetDeleteV1: case SyncEntityType.assetDeleteV1:
await _runWithManageMediaPermission(
logContext: "Deleted Assets",
action: () async {
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
await _handleRemoteDeleted(remoteSyncAssets.map((e) => e.assetId));
},
);
return _syncStreamRepository.deleteAssetsV1(data.cast()); return _syncStreamRepository.deleteAssetsV1(data.cast());
case SyncEntityType.assetExifV1: case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast()); return _syncStreamRepository.updateAssetsExifV1(data.cast());
@@ -388,32 +382,28 @@ class SyncStreamService {
} }
} }
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async { Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
if (remoteIds.isEmpty) { if (checksums.isEmpty) {
return Future.value(); return Future.value();
} else { } else {
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds); final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
await _trashLocalAssets(localAssetsToTrash); final mediaUrls = await Future.wait(
localAssetsToTrash.values
.expand((e) => e)
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
);
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
}
} else { } else {
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds"); _logger.info("No assets found in backup-enabled albums for assets: $checksums");
} }
} }
} }
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
final mediaUrls = await Future.wait(
localAssetsToTrash.values
.expand((e) => e)
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
);
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
}
}
Future<void> _applyRemoteRestoreToLocal() async { Future<void> _applyRemoteRestoreToLocal() async {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore(); final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) { if (assetsToRestore.isNotEmpty) {
@@ -423,21 +413,4 @@ class SyncStreamService {
_logger.info("No remote assets found for restoration"); _logger.info("No remote assets found for restoration");
} }
} }
Future<void> _runWithManageMediaPermission({
required String logContext,
required Future<void> Function() action,
}) async {
if (!CurrentPlatform.isAndroid || !Store.get(StoreKey.manageLocalMediaAndroid, false)) {
return;
}
final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (!hasPermission) {
_logger.warning("sync $logContext cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await action();
}
} }
@@ -109,40 +109,31 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get(); return query.map((localAlbum) => localAlbum.toDto()).get();
} }
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> remoteIds) async { Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
if (remoteIds.isEmpty) { if (checksums.isEmpty) {
return {}; return {};
} }
final result = <String, List<LocalAsset>>{}; final result = <String, List<LocalAsset>>{};
for (final slice in remoteIds.toSet().slices(kDriftMaxChunk)) { for (final slice in checksums.toSet().slices(kDriftMaxChunk)) {
final rows = final rows =
await (_db.select(_db.localAlbumAssetEntity).join([ await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin( innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
_db.localAlbumEntity,
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
useColumns: false,
),
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
innerJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
])..where( ])..where(
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
_db.remoteAssetEntity.id.isIn(slice), _db.localAssetEntity.checksum.isIn(slice),
)) ))
.get(); .get();
for (final row in rows) { for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final asset = row.readTable(_db.localAssetEntity).toDto(); final assetData = row.readTable(_db.localAssetEntity);
final asset = assetData.toDto();
(result[albumId] ??= <LocalAsset>[]).add(asset); (result[albumId] ??= <LocalAsset>[]).add(asset);
} }
} }
return result; return result;
} }
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart';
@@ -364,8 +363,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
} }
BaseAsset displayAsset = asset; BaseAsset displayAsset = asset;
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash)); final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
if (stackChildren != null && stackChildren.isNotEmpty) { if (stackChildren != null && stackChildren.isNotEmpty) {
displayAsset = stackChildren.elementAt(stackIndex); displayAsset = stackChildren.elementAt(stackIndex);
} }
@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class AssetStackRow extends ConsumerWidget { class AssetStackRow extends ConsumerWidget {
final List<RemoteAsset> stack; final List<RemoteAsset> stack;
@@ -17,11 +15,6 @@ class AssetStackRow extends ConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
if (hideAssetStack) {
return const SizedBox.shrink();
}
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
@@ -2,21 +2,17 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
class MapBottomSheet extends StatelessWidget { class MapBottomSheet extends StatelessWidget {
final Key? sheetKey; const MapBottomSheet({super.key});
const MapBottomSheet({super.key, this.sheetKey});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BaseBottomSheet( return BaseBottomSheet(
key: sheetKey,
initialChildSize: 0.25, initialChildSize: 0.25,
maxChildSize: 0.75, maxChildSize: 0.75,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
@@ -53,7 +49,7 @@ class _ScopedMapTimeline extends StatelessWidget {
return timelineService; return timelineService;
}), }),
], ],
child: const Timeline(appBar: null, bottomSheet: GeneralBottomSheet(minChildSize: 0.23), withScrubber: false), child: const Timeline(appBar: null, bottomSheet: null, withScrubber: false),
); );
} }
} }
@@ -21,7 +21,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
this.showStorageIndicator = false, this.showStorageIndicator = false,
this.lockSelection = false, this.lockSelection = false,
this.heroOffset, this.heroOffset,
this.showStackIndicator = false,
super.key, super.key,
}); });
@@ -31,7 +30,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
final bool showStorageIndicator; final bool showStorageIndicator;
final bool lockSelection; final bool lockSelection;
final int? heroOffset; final int? heroOffset;
final bool showStackIndicator;
@override @override
ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState(); ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState();
@@ -141,14 +139,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
duration: Durations.short4, duration: Durations.short4,
child: Align( child: Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: Column( child: _AssetTypeIcons(asset: asset),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_AssetTypeIcons(asset: asset),
if (widget.showStackIndicator) _StackIndicator(asset: asset),
],
),
), ),
), ),
if (storageIndicator && asset != null) if (storageIndicator && asset != null)
@@ -295,8 +286,8 @@ class _AssetTypeIcons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null; final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
final isLivePhoto = remoteAsset?.livePhotoVideoId != null; final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -304,6 +295,11 @@ class _AssetTypeIcons extends StatelessWidget {
children: [ children: [
if (asset.isVideo) if (asset.isVideo)
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)), Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
if (hasStack)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
),
if (isLivePhoto) if (isLivePhoto)
const Padding( const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0), padding: EdgeInsets.only(right: 10.0, top: 6.0),
@@ -316,24 +312,6 @@ class _AssetTypeIcons extends StatelessWidget {
} }
} }
class _StackIndicator extends StatelessWidget {
final BaseAsset asset;
const _StackIndicator({required this.asset});
@override
Widget build(BuildContext context) {
if (asset is! RemoteAsset || (asset as RemoteAsset).stackId == null) {
return const SizedBox.shrink();
}
return const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
);
}
}
class _UploadProgressOverlay extends StatelessWidget { class _UploadProgressOverlay extends StatelessWidget {
final double progress; final double progress;
@@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart'; import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
@@ -54,7 +53,6 @@ class _DriftMapState extends ConsumerState<DriftMap> {
final _reloadMutex = AsyncMutex(); final _reloadMutex = AsyncMutex();
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25); final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
final GlobalKey _bottomSheetKey = GlobalKey();
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
@override @override
@@ -186,7 +184,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
return Stack( return Stack(
children: [ children: [
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady), _Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset, sheetKey: _bottomSheetKey), _DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset), _DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
], ],
); );
@@ -226,9 +224,8 @@ class _Map extends StatelessWidget {
class _DynamicBottomSheet extends StatefulWidget { class _DynamicBottomSheet extends StatefulWidget {
final ValueNotifier<double> bottomSheetOffset; final ValueNotifier<double> bottomSheetOffset;
final GlobalKey sheetKey;
const _DynamicBottomSheet({required this.bottomSheetOffset, required this.sheetKey}); const _DynamicBottomSheet({required this.bottomSheetOffset});
@override @override
State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState(); State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState();
@@ -239,13 +236,10 @@ class _DynamicBottomSheetState extends State<_DynamicBottomSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return NotificationListener<DraggableScrollableNotification>( return NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) { onNotification: (notification) {
final sheet = notification.context.findAncestorWidgetOfExactType<BaseBottomSheet>(); widget.bottomSheetOffset.value = notification.extent;
if (sheet?.key == widget.sheetKey) { return true;
widget.bottomSheetOffset.value = notification.extent;
}
return false;
}, },
child: MapBottomSheet(sheetKey: widget.sheetKey), child: const MapBottomSheet(),
); );
} }
} }
@@ -244,7 +244,6 @@ class _AssetTileWidget extends ConsumerWidget {
final lockSelection = _getLockSelectionStatus(ref); final lockSelection = _getLockSelectionStatus(ref);
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator)); final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash;
return RepaintBoundary( return RepaintBoundary(
child: GestureDetector( child: GestureDetector(
@@ -254,7 +253,6 @@ class _AssetTileWidget extends ConsumerWidget {
asset, asset,
lockSelection: lockSelection, lockSelection: lockSelection,
showStorageIndicator: showStorageIndicator, showStorageIndicator: showStorageIndicator,
showStackIndicator: showStackIndicator,
heroOffset: heroOffset, heroOffset: heroOffset,
), ),
), ),
@@ -469,7 +469,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
ref.read(timelineStateProvider.notifier).setScrolling(true); ref.read(timelineStateProvider.notifier).setScrolling(true);
}, },
child: Stack( child: Stack(
clipBehavior: Clip.none,
children: [ children: [
timeline, timeline,
if (isBottomWidgetVisible) if (isBottomWidgetVisible)
@@ -148,7 +148,6 @@ enum ActionButtonType {
context.selectedCount == 1, context.selectedCount == 1,
ActionButtonType.unstack => ActionButtonType.unstack =>
context.isOwner && // context.isOwner && //
context.timelineOrigin != TimelineOrigin.trash &&
!context.isInLockedView && // !context.isInLockedView && //
context.isStacked, context.isStacked,
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView, ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
+6 -6
View File
@@ -183,15 +183,15 @@ class PeopleApi {
/// * [String] closestPersonId: /// * [String] closestPersonId:
/// Closest person ID for similarity search /// Closest person ID for similarity search
/// ///
/// * [int] page: /// * [num] page:
/// Page number for pagination /// Page number for pagination
/// ///
/// * [int] size: /// * [num] size:
/// Number of items per page /// Number of items per page
/// ///
/// * [bool] withHidden: /// * [bool] withHidden:
/// Include hidden people /// Include hidden people
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/people'; final apiPath = r'/people';
@@ -244,15 +244,15 @@ class PeopleApi {
/// * [String] closestPersonId: /// * [String] closestPersonId:
/// Closest person ID for similarity search /// Closest person ID for similarity search
/// ///
/// * [int] page: /// * [num] page:
/// Page number for pagination /// Page number for pagination
/// ///
/// * [int] size: /// * [num] size:
/// Number of items per page /// Number of items per page
/// ///
/// * [bool] withHidden: /// * [bool] withHidden:
/// Include hidden people /// Include hidden people
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+6 -6
View File
@@ -404,10 +404,10 @@ class SearchApi {
/// * [List<String>] personIds: /// * [List<String>] personIds:
/// Filter by person IDs /// Filter by person IDs
/// ///
/// * [int] rating: /// * [num] rating:
/// Filter by rating [1-5], or null for unrated /// Filter by rating [1-5], or null for unrated
/// ///
/// * [int] size: /// * [num] size:
/// Number of results to return /// Number of results to return
/// ///
/// * [String] state: /// * [String] state:
@@ -443,7 +443,7 @@ class SearchApi {
/// ///
/// * [bool] withExif: /// * [bool] withExif:
/// Include EXIF data in response /// Include EXIF data in response
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/search/large-assets'; final apiPath = r'/search/large-assets';
@@ -619,10 +619,10 @@ class SearchApi {
/// * [List<String>] personIds: /// * [List<String>] personIds:
/// Filter by person IDs /// Filter by person IDs
/// ///
/// * [int] rating: /// * [num] rating:
/// Filter by rating [1-5], or null for unrated /// Filter by rating [1-5], or null for unrated
/// ///
/// * [int] size: /// * [num] size:
/// Number of results to return /// Number of results to return
/// ///
/// * [String] state: /// * [String] state:
@@ -658,7 +658,7 @@ class SearchApi {
/// ///
/// * [bool] withExif: /// * [bool] withExif:
/// Include EXIF data in response /// Include EXIF data in response
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+20 -10
View File
@@ -157,10 +157,14 @@ class AlbumResponseDto {
json[r'albumUsers'] = this.albumUsers; json[r'albumUsers'] = this.albumUsers;
json[r'assetCount'] = this.assetCount; json[r'assetCount'] = this.assetCount;
json[r'contributorCounts'] = this.contributorCounts; json[r'contributorCounts'] = this.contributorCounts;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
json[r'description'] = this.description; json[r'description'] = this.description;
if (this.endDate != null) { if (this.endDate != null) {
json[r'endDate'] = this.endDate!.toUtc().toIso8601String(); json[r'endDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.endDate!.millisecondsSinceEpoch
: this.endDate!.toUtc().toIso8601String();
} else { } else {
// json[r'endDate'] = null; // json[r'endDate'] = null;
} }
@@ -168,7 +172,9 @@ class AlbumResponseDto {
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isActivityEnabled'] = this.isActivityEnabled; json[r'isActivityEnabled'] = this.isActivityEnabled;
if (this.lastModifiedAssetTimestamp != null) { if (this.lastModifiedAssetTimestamp != null) {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); json[r'lastModifiedAssetTimestamp'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.lastModifiedAssetTimestamp!.millisecondsSinceEpoch
: this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else { } else {
// json[r'lastModifiedAssetTimestamp'] = null; // json[r'lastModifiedAssetTimestamp'] = null;
} }
@@ -179,11 +185,15 @@ class AlbumResponseDto {
} }
json[r'shared'] = this.shared; json[r'shared'] = this.shared;
if (this.startDate != null) { if (this.startDate != null) {
json[r'startDate'] = this.startDate!.toUtc().toIso8601String(); json[r'startDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.startDate!.millisecondsSinceEpoch
: this.startDate!.toUtc().toIso8601String();
} else { } else {
// json[r'startDate'] = null; // json[r'startDate'] = null;
} }
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
return json; return json;
} }
@@ -201,17 +211,17 @@ class AlbumResponseDto {
albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
assetCount: mapValueOfType<int>(json, r'assetCount')!, assetCount: mapValueOfType<int>(json, r'assetCount')!,
contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']),
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
description: mapValueOfType<String>(json, r'description')!, description: mapValueOfType<String>(json, r'description')!,
endDate: mapDateTime(json, r'endDate', r''), endDate: mapDateTime(json, r'endDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!, hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!, isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
order: AssetOrder.fromJson(json[r'order']), order: AssetOrder.fromJson(json[r'order']),
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,
startDate: mapDateTime(json, r'startDate', r''), startDate: mapDateTime(json, r'startDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
); );
} }
return null; return null;
+2 -5
View File
@@ -37,15 +37,12 @@ class AssetBulkUpdateDto {
/// Relative time offset in seconds /// Relative time offset in seconds
/// ///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? dateTimeRelative; num? dateTimeRelative;
/// Asset description /// Asset description
/// ///
@@ -216,7 +213,7 @@ class AssetBulkUpdateDto {
return AssetBulkUpdateDto( return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'), dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'), dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
duplicateId: mapValueOfType<String>(json, r'duplicateId'), duplicateId: mapValueOfType<String>(json, r'duplicateId'),
ids: json[r'ids'] is Iterable ids: json[r'ids'] is Iterable
@@ -24,26 +24,22 @@ class AssetEditActionItemDtoParameters {
/// Height of the crop /// Height of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num height;
int height;
/// Width of the crop /// Width of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num width;
int width;
/// Top-Left X coordinate of crop /// Top-Left X coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num x;
int x;
/// Top-Left Y coordinate of crop /// Top-Left Y coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num y;
int y;
/// Rotation angle in degrees /// Rotation angle in degrees
num angle; num angle;
@@ -92,10 +88,10 @@ class AssetEditActionItemDtoParameters {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AssetEditActionItemDtoParameters( return AssetEditActionItemDtoParameters(
height: mapValueOfType<int>(json, r'height')!, height: num.parse('${json[r'height']}'),
width: mapValueOfType<int>(json, r'width')!, width: num.parse('${json[r'width']}'),
x: mapValueOfType<int>(json, r'x')!, x: num.parse('${json[r'x']}'),
y: mapValueOfType<int>(json, r'y')!, y: num.parse('${json[r'y']}'),
angle: num.parse('${json[r'angle']}'), angle: num.parse('${json[r'angle']}'),
axis: MirrorAxis.fromJson(json[r'axis'])!, axis: MirrorAxis.fromJson(json[r'axis'])!,
); );
+28 -16
View File
@@ -80,8 +80,7 @@ class AssetResponseDto {
/// Asset height /// Asset height
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? height;
int? height;
/// Asset ID /// Asset ID
String id; String id;
@@ -166,8 +165,7 @@ class AssetResponseDto {
/// Asset width /// Asset width
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? width;
int? width;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
@@ -248,7 +246,9 @@ class AssetResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'checksum'] = this.checksum; json[r'checksum'] = this.checksum;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
if (this.duplicateId != null) { if (this.duplicateId != null) {
json[r'duplicateId'] = this.duplicateId; json[r'duplicateId'] = this.duplicateId;
} else { } else {
@@ -264,8 +264,12 @@ class AssetResponseDto {
} else { } else {
// json[r'exifInfo'] = null; // json[r'exifInfo'] = null;
} }
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); ? this.fileCreatedAt.millisecondsSinceEpoch
: this.fileCreatedAt.toUtc().toIso8601String();
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.fileModifiedAt.millisecondsSinceEpoch
: this.fileModifiedAt.toUtc().toIso8601String();
json[r'hasMetadata'] = this.hasMetadata; json[r'hasMetadata'] = this.hasMetadata;
if (this.height != null) { if (this.height != null) {
json[r'height'] = this.height; json[r'height'] = this.height;
@@ -288,7 +292,9 @@ class AssetResponseDto {
} else { } else {
// json[r'livePhotoVideoId'] = null; // json[r'livePhotoVideoId'] = null;
} }
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String(); json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.localDateTime.millisecondsSinceEpoch
: this.localDateTime.toUtc().toIso8601String();
json[r'originalFileName'] = this.originalFileName; json[r'originalFileName'] = this.originalFileName;
if (this.originalMimeType != null) { if (this.originalMimeType != null) {
json[r'originalMimeType'] = this.originalMimeType; json[r'originalMimeType'] = this.originalMimeType;
@@ -321,7 +327,9 @@ class AssetResponseDto {
} }
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'unassignedFaces'] = this.unassignedFaces; json[r'unassignedFaces'] = this.unassignedFaces;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
json[r'visibility'] = this.visibility; json[r'visibility'] = this.visibility;
if (this.width != null) { if (this.width != null) {
json[r'width'] = this.width; json[r'width'] = this.width;
@@ -341,14 +349,16 @@ class AssetResponseDto {
return AssetResponseDto( return AssetResponseDto(
checksum: mapValueOfType<String>(json, r'checksum')!, checksum: mapValueOfType<String>(json, r'checksum')!,
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
duplicateId: mapValueOfType<String>(json, r'duplicateId'), duplicateId: mapValueOfType<String>(json, r'duplicateId'),
duration: mapValueOfType<String>(json, r'duration'), duration: mapValueOfType<String>(json, r'duration'),
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!, hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
height: mapValueOfType<int>(json, r'height'), height: json[r'height'] == null
? null
: num.parse('${json[r'height']}'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!, isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isEdited: mapValueOfType<bool>(json, r'isEdited')!, isEdited: mapValueOfType<bool>(json, r'isEdited')!,
@@ -357,7 +367,7 @@ class AssetResponseDto {
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!, isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'')!, localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
originalFileName: mapValueOfType<String>(json, r'originalFileName')!, originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalMimeType: mapValueOfType<String>(json, r'originalMimeType'), originalMimeType: mapValueOfType<String>(json, r'originalMimeType'),
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
@@ -370,9 +380,11 @@ class AssetResponseDto {
thumbhash: mapValueOfType<String>(json, r'thumbhash'), thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!, type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: mapValueOfType<int>(json, r'width'), width: json[r'width'] == null
? null
: num.parse('${json[r'width']}'),
); );
} }
return null; return null;
+8 -12
View File
@@ -22,26 +22,22 @@ class CropParameters {
/// Height of the crop /// Height of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num height;
int height;
/// Width of the crop /// Width of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num width;
int width;
/// Top-Left X coordinate of crop /// Top-Left X coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num x;
int x;
/// Top-Left Y coordinate of crop /// Top-Left Y coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num y;
int y;
@override @override
bool operator ==(Object other) => identical(this, other) || other is CropParameters && bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
@@ -79,10 +75,10 @@ class CropParameters {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return CropParameters( return CropParameters(
height: mapValueOfType<int>(json, r'height')!, height: num.parse('${json[r'height']}'),
width: mapValueOfType<int>(json, r'width')!, width: num.parse('${json[r'width']}'),
x: mapValueOfType<int>(json, r'x')!, x: num.parse('${json[r'x']}'),
y: mapValueOfType<int>(json, r'y')!, y: num.parse('${json[r'y']}'),
); );
} }
return null; return null;
+2 -3
View File
@@ -27,8 +27,7 @@ class DatabaseBackupConfig {
/// Keep last amount /// Keep last amount
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num keepLastAmount;
int keepLastAmount;
@override @override
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig && bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
@@ -65,7 +64,7 @@ class DatabaseBackupConfig {
return DatabaseBackupConfig( return DatabaseBackupConfig(
cronExpression: mapValueOfType<String>(json, r'cronExpression')!, cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!, keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
); );
} }
return null; return null;
+2 -5
View File
@@ -22,10 +22,7 @@ class DatabaseBackupDto {
String filename; String filename;
/// Backup file size /// Backup file size
/// num filesize;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int filesize;
/// Backup timezone /// Backup timezone
String timezone; String timezone;
@@ -64,7 +61,7 @@ class DatabaseBackupDto {
return DatabaseBackupDto( return DatabaseBackupDto(
filename: mapValueOfType<String>(json, r'filename')!, filename: mapValueOfType<String>(json, r'filename')!,
filesize: mapValueOfType<int>(json, r'filesize')!, filesize: num.parse('${json[r'filesize']}'),
timezone: mapValueOfType<String>(json, r'timezone')!, timezone: mapValueOfType<String>(json, r'timezone')!,
); );
} }
+24 -20
View File
@@ -52,14 +52,12 @@ class ExifResponseDto {
/// Image height in pixels /// Image height in pixels
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? exifImageHeight;
int? exifImageHeight;
/// Image width in pixels /// Image width in pixels
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? exifImageWidth;
int? exifImageWidth;
/// Exposure time /// Exposure time
String? exposureTime; String? exposureTime;
@@ -77,10 +75,7 @@ class ExifResponseDto {
num? focalLength; num? focalLength;
/// ISO sensitivity /// ISO sensitivity
/// num? iso;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? iso;
/// GPS latitude /// GPS latitude
num? latitude; num? latitude;
@@ -107,10 +102,7 @@ class ExifResponseDto {
String? projectionType; String? projectionType;
/// Rating /// Rating
/// num? rating;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? rating;
/// State/province name /// State/province name
String? state; String? state;
@@ -185,7 +177,9 @@ class ExifResponseDto {
// json[r'country'] = null; // json[r'country'] = null;
} }
if (this.dateTimeOriginal != null) { if (this.dateTimeOriginal != null) {
json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.dateTimeOriginal!.millisecondsSinceEpoch
: this.dateTimeOriginal!.toUtc().toIso8601String();
} else { } else {
// json[r'dateTimeOriginal'] = null; // json[r'dateTimeOriginal'] = null;
} }
@@ -255,7 +249,9 @@ class ExifResponseDto {
// json[r'model'] = null; // json[r'model'] = null;
} }
if (this.modifyDate != null) { if (this.modifyDate != null) {
json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.modifyDate!.millisecondsSinceEpoch
: this.modifyDate!.toUtc().toIso8601String();
} else { } else {
// json[r'modifyDate'] = null; // json[r'modifyDate'] = null;
} }
@@ -298,10 +294,14 @@ class ExifResponseDto {
return ExifResponseDto( return ExifResponseDto(
city: mapValueOfType<String>(json, r'city'), city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'), exifImageHeight: json[r'exifImageHeight'] == null
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'), ? null
: num.parse('${json[r'exifImageHeight']}'),
exifImageWidth: json[r'exifImageWidth'] == null
? null
: num.parse('${json[r'exifImageWidth']}'),
exposureTime: mapValueOfType<String>(json, r'exposureTime'), exposureTime: mapValueOfType<String>(json, r'exposureTime'),
fNumber: json[r'fNumber'] == null fNumber: json[r'fNumber'] == null
? null ? null
@@ -310,7 +310,9 @@ class ExifResponseDto {
focalLength: json[r'focalLength'] == null focalLength: json[r'focalLength'] == null
? null ? null
: num.parse('${json[r'focalLength']}'), : num.parse('${json[r'focalLength']}'),
iso: mapValueOfType<int>(json, r'iso'), iso: json[r'iso'] == null
? null
: num.parse('${json[r'iso']}'),
latitude: json[r'latitude'] == null latitude: json[r'latitude'] == null
? null ? null
: num.parse('${json[r'latitude']}'), : num.parse('${json[r'latitude']}'),
@@ -320,10 +322,12 @@ class ExifResponseDto {
: num.parse('${json[r'longitude']}'), : num.parse('${json[r'longitude']}'),
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
modifyDate: mapDateTime(json, r'modifyDate', r''), modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
orientation: mapValueOfType<String>(json, r'orientation'), orientation: mapValueOfType<String>(json, r'orientation'),
projectionType: mapValueOfType<String>(json, r'projectionType'), projectionType: mapValueOfType<String>(json, r'projectionType'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
timeZone: mapValueOfType<String>(json, r'timeZone'), timeZone: mapValueOfType<String>(json, r'timeZone'),
); );
@@ -21,13 +21,9 @@ class MachineLearningAvailabilityChecksDto {
/// Enabled /// Enabled
bool enabled; bool enabled;
/// Minimum value: -9007199254740991 num interval;
/// Maximum value: 9007199254740991
int interval;
/// Minimum value: -9007199254740991 num timeout;
/// Maximum value: 9007199254740991
int timeout;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto && bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
@@ -63,8 +59,8 @@ class MachineLearningAvailabilityChecksDto {
return MachineLearningAvailabilityChecksDto( return MachineLearningAvailabilityChecksDto(
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
interval: mapValueOfType<int>(json, r'interval')!, interval: num.parse('${json[r'interval']}'),
timeout: mapValueOfType<int>(json, r'timeout')!, timeout: num.parse('${json[r'timeout']}'),
); );
} }
return null; return null;
@@ -20,10 +20,7 @@ class MaintenanceDetectInstallStorageFolderDto {
}); });
/// Number of files in the folder /// Number of files in the folder
/// num files;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int files;
StorageFolder folder; StorageFolder folder;
@@ -69,7 +66,7 @@ class MaintenanceDetectInstallStorageFolderDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MaintenanceDetectInstallStorageFolderDto( return MaintenanceDetectInstallStorageFolderDto(
files: mapValueOfType<int>(json, r'files')!, files: num.parse('${json[r'files']}'),
folder: StorageFolder.fromJson(json[r'folder'])!, folder: StorageFolder.fromJson(json[r'folder'])!,
readable: mapValueOfType<bool>(json, r'readable')!, readable: mapValueOfType<bool>(json, r'readable')!,
writable: mapValueOfType<bool>(json, r'writable')!, writable: mapValueOfType<bool>(json, r'writable')!,
@@ -32,15 +32,13 @@ class MaintenanceStatusResponseDto {
/// ///
String? error; String? error;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? progress; num? progress;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -104,7 +102,7 @@ class MaintenanceStatusResponseDto {
action: MaintenanceAction.fromJson(json[r'action'])!, action: MaintenanceAction.fromJson(json[r'action'])!,
active: mapValueOfType<bool>(json, r'active')!, active: mapValueOfType<bool>(json, r'active')!,
error: mapValueOfType<String>(json, r'error'), error: mapValueOfType<String>(json, r'error'),
progress: mapValueOfType<int>(json, r'progress'), progress: num.parse('${json[r'progress']}'),
task: mapValueOfType<String>(json, r'task'), task: mapValueOfType<String>(json, r'task'),
); );
} }
+8 -7
View File
@@ -215,14 +215,13 @@ class MetadataSearchDto {
/// Page number /// Page number
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? page; num? page;
/// Filter by person IDs /// Filter by person IDs
List<String> personIds; List<String> personIds;
@@ -240,7 +239,7 @@ class MetadataSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -252,7 +251,7 @@ class MetadataSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -725,13 +724,15 @@ class MetadataSearchDto {
order: AssetOrder.fromJson(json[r'order']), order: AssetOrder.fromJson(json[r'order']),
originalFileName: mapValueOfType<String>(json, r'originalFileName'), originalFileName: mapValueOfType<String>(json, r'originalFileName'),
originalPath: mapValueOfType<String>(json, r'originalPath'), originalPath: mapValueOfType<String>(json, r'originalPath'),
page: mapValueOfType<int>(json, r'page'), page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
previewPath: mapValueOfType<String>(json, r'previewPath'), previewPath: mapValueOfType<String>(json, r'previewPath'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+4 -2
View File
@@ -83,7 +83,9 @@ class PartnerResponseDto {
// json[r'inTimeline'] = null; // json[r'inTimeline'] = null;
} }
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
return json; return json;
} }
@@ -102,7 +104,7 @@ class PartnerResponseDto {
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
inTimeline: mapValueOfType<bool>(json, r'inTimeline'), inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
); );
} }
+8 -4
View File
@@ -94,7 +94,9 @@ class PersonResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.birthDate != null) { if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
? this.birthDate!.millisecondsSinceEpoch
: _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
@@ -113,7 +115,9 @@ class PersonResponseDto {
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath; json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) { if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt!.millisecondsSinceEpoch
: this.updatedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'updatedAt'] = null; // json[r'updatedAt'] = null;
} }
@@ -129,14 +133,14 @@ class PersonResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PersonResponseDto( return PersonResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!, thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''), updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
); );
} }
return null; return null;
@@ -99,7 +99,9 @@ class PersonWithFacesResponseDto {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.birthDate != null) { if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/')
? this.birthDate!.millisecondsSinceEpoch
: _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
@@ -119,7 +121,9 @@ class PersonWithFacesResponseDto {
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath; json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) { if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt!.millisecondsSinceEpoch
: this.updatedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'updatedAt'] = null; // json[r'updatedAt'] = null;
} }
@@ -135,7 +139,7 @@ class PersonWithFacesResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return PersonWithFacesResponseDto( return PersonWithFacesResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$/'),
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']), faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
@@ -143,7 +147,7 @@ class PersonWithFacesResponseDto {
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!, thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''), updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
); );
} }
return null; return null;
+6 -4
View File
@@ -147,7 +147,7 @@ class RandomSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -159,7 +159,7 @@ class RandomSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -549,8 +549,10 @@ class RandomSearchDto {
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+2 -3
View File
@@ -39,14 +39,13 @@ class SessionCreateDto {
/// Session duration in seconds /// Session duration in seconds
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? duration; num? duration;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto && bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
@@ -95,7 +94,7 @@ class SessionCreateDto {
return SessionCreateDto( return SessionCreateDto(
deviceOS: mapValueOfType<String>(json, r'deviceOS'), deviceOS: mapValueOfType<String>(json, r'deviceOS'),
deviceType: mapValueOfType<String>(json, r'deviceType'), deviceType: mapValueOfType<String>(json, r'deviceType'),
duration: mapValueOfType<int>(json, r'duration'), duration: num.parse('${json[r'duration']}'),
); );
} }
return null; return null;
+8 -7
View File
@@ -154,14 +154,13 @@ class SmartSearchDto {
/// Page number /// Page number
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? page; num? page;
/// Filter by person IDs /// Filter by person IDs
List<String> personIds; List<String> personIds;
@@ -188,7 +187,7 @@ class SmartSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -200,7 +199,7 @@ class SmartSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -584,14 +583,16 @@ class SmartSearchDto {
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
ocr: mapValueOfType<String>(json, r'ocr'), ocr: mapValueOfType<String>(json, r'ocr'),
page: mapValueOfType<int>(json, r'page'), page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
query: mapValueOfType<String>(json, r'query'), query: mapValueOfType<String>(json, r'query'),
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'), queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+4 -2
View File
@@ -152,7 +152,7 @@ class StatisticsSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -479,7 +479,9 @@ class StatisticsSearchDto {
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+4 -3
View File
@@ -57,8 +57,7 @@ class SystemConfigOAuthDto {
/// Default storage quota /// Default storage quota
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? defaultStorageQuota;
int? defaultStorageQuota;
/// Enabled /// Enabled
bool enabled; bool enabled;
@@ -201,7 +200,9 @@ class SystemConfigOAuthDto {
buttonText: mapValueOfType<String>(json, r'buttonText')!, buttonText: mapValueOfType<String>(json, r'buttonText')!,
clientId: mapValueOfType<String>(json, r'clientId')!, clientId: mapValueOfType<String>(json, r'clientId')!,
clientSecret: mapValueOfType<String>(json, r'clientSecret')!, clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
defaultStorageQuota: mapValueOfType<int>(json, r'defaultStorageQuota'), defaultStorageQuota: json[r'defaultStorageQuota'] == null
? null
: num.parse('${json[r'defaultStorageQuota']}'),
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!, endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!,
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!, issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
@@ -34,7 +34,7 @@ class SystemConfigSmtpTransportDto {
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 65535 /// Maximum value: 65535
int port; num port;
/// Whether to use secure connection (TLS/SSL) /// Whether to use secure connection (TLS/SSL)
bool secure; bool secure;
@@ -87,7 +87,7 @@ class SystemConfigSmtpTransportDto {
host: mapValueOfType<String>(json, r'host')!, host: mapValueOfType<String>(json, r'host')!,
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!, ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
password: mapValueOfType<String>(json, r'password')!, password: mapValueOfType<String>(json, r'password')!,
port: mapValueOfType<int>(json, r'port')!, port: num.parse('${json[r'port']}'),
secure: mapValueOfType<bool>(json, r'secure')!, secure: mapValueOfType<bool>(json, r'secure')!,
username: mapValueOfType<String>(json, r'username')!, username: mapValueOfType<String>(json, r'username')!,
); );
+8 -4
View File
@@ -86,7 +86,9 @@ class TagResponseDto {
} else { } else {
// json[r'color'] = null; // json[r'color'] = null;
} }
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.createdAt.millisecondsSinceEpoch
: this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
if (this.parentId != null) { if (this.parentId != null) {
@@ -94,7 +96,9 @@ class TagResponseDto {
} else { } else {
// json[r'parentId'] = null; // json[r'parentId'] = null;
} }
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.updatedAt.millisecondsSinceEpoch
: this.updatedAt.toUtc().toIso8601String();
json[r'value'] = this.value; json[r'value'] = this.value;
return json; return json;
} }
@@ -109,11 +113,11 @@ class TagResponseDto {
return TagResponseDto( return TagResponseDto(
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
parentId: mapValueOfType<String>(json, r'parentId'), parentId: mapValueOfType<String>(json, r'parentId'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
value: mapValueOfType<String>(json, r'value')!, value: mapValueOfType<String>(json, r'value')!,
); );
} }
+4 -2
View File
@@ -153,7 +153,9 @@ class UserAdminResponseDto {
} }
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId; json[r'oauthId'] = this.oauthId;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
if (this.quotaSizeInBytes != null) { if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
@@ -196,7 +198,7 @@ class UserAdminResponseDto {
license: UserLicense.fromJson(json[r'license']), license: UserLicense.fromJson(json[r'license']),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!, oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'), quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'), quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
+4 -2
View File
@@ -66,7 +66,9 @@ class UserResponseDto {
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.profileChangedAt.millisecondsSinceEpoch
: this.profileChangedAt.toUtc().toIso8601String();
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
return json; return json;
} }
@@ -84,7 +86,7 @@ class UserResponseDto {
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
); );
} }
+2 -5
View File
@@ -26,10 +26,7 @@ class WorkflowActionResponseDto {
String id; String id;
/// Action order /// Action order
/// num order;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int order;
/// Plugin action ID /// Plugin action ID
String pluginActionId; String pluginActionId;
@@ -82,7 +79,7 @@ class WorkflowActionResponseDto {
return WorkflowActionResponseDto( return WorkflowActionResponseDto(
actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'), actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
order: mapValueOfType<int>(json, r'order')!, order: num.parse('${json[r'order']}'),
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!, pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
workflowId: mapValueOfType<String>(json, r'workflowId')!, workflowId: mapValueOfType<String>(json, r'workflowId')!,
); );
+2 -5
View File
@@ -26,10 +26,7 @@ class WorkflowFilterResponseDto {
String id; String id;
/// Filter order /// Filter order
/// num order;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int order;
/// Plugin filter ID /// Plugin filter ID
String pluginFilterId; String pluginFilterId;
@@ -82,7 +79,7 @@ class WorkflowFilterResponseDto {
return WorkflowFilterResponseDto( return WorkflowFilterResponseDto(
filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'), filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
order: mapValueOfType<int>(json, r'order')!, order: num.parse('${json[r'order']}'),
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!, pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
workflowId: mapValueOfType<String>(json, r'workflowId')!, workflowId: mapValueOfType<String>(json, r'workflowId')!,
); );
@@ -419,8 +419,8 @@ void main() {
'album-b': [mergedAsset], 'album-b': [mergedAsset],
}; };
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async { when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>; final Iterable<String> requestedChecksums = invocation.positionalArguments.first as Iterable<String>;
expect(requestedRemoteIds.toSet(), equals({'remote-1', 'remote-2', 'remote-3'})); expect(requestedChecksums.toSet(), equals({'checksum-local', 'checksum-merged', 'checksum-remote-only'}));
return assetsByAlbum; return assetsByAlbum;
}); });
@@ -482,18 +482,12 @@ void main() {
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any())); verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
}); });
test("requests local deletions lookup by remote ids for permanent remote delete events", () async { test("does not request local deletions for permanent remote delete events", () async {
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>;
expect(requestedRemoteIds.toSet(), equals({'remote-asset'}));
return {};
});
final events = [SyncStreamStub.assetDeleteV1]; final events = [SyncStreamStub.assetDeleteV1];
await simulateEvents(events); await simulateEvents(events);
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).called(1); verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any()));
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any())); verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1); verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
}); });
+77 -69
View File
@@ -7964,9 +7964,8 @@
"description": "Page number for pagination", "description": "Page number for pagination",
"schema": { "schema": {
"minimum": 1, "minimum": 1,
"maximum": 9007199254740991,
"default": 1, "default": 1,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -7978,7 +7977,7 @@
"minimum": 1, "minimum": 1,
"maximum": 1000, "maximum": 1000,
"default": 500, "default": 500,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -9373,7 +9372,7 @@
], ],
"x-immich-state": "Stable", "x-immich-state": "Stable",
"schema": { "schema": {
"type": "integer", "type": "number",
"minimum": -1, "minimum": -1,
"maximum": 5, "maximum": 5,
"nullable": true "nullable": true
@@ -9387,7 +9386,7 @@
"schema": { "schema": {
"minimum": 1, "minimum": 1,
"maximum": 1000, "maximum": 1000,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -15301,7 +15300,9 @@
}, },
"createdAt": { "createdAt": {
"description": "Creation date", "description": "Creation date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"description": { "description": {
@@ -15310,7 +15311,9 @@
}, },
"endDate": { "endDate": {
"description": "End date (latest asset)", "description": "End date (latest asset)",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"hasSharedLink": { "hasSharedLink": {
@@ -15327,7 +15330,9 @@
}, },
"lastModifiedAssetTimestamp": { "lastModifiedAssetTimestamp": {
"description": "Last modified asset timestamp", "description": "Last modified asset timestamp",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"order": { "order": {
@@ -15339,12 +15344,16 @@
}, },
"startDate": { "startDate": {
"description": "Start date (earliest asset)", "description": "Start date (earliest asset)",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
} }
}, },
@@ -15637,9 +15646,7 @@
}, },
"dateTimeRelative": { "dateTimeRelative": {
"description": "Relative time offset in seconds", "description": "Relative time offset in seconds",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"description": { "description": {
"description": "Asset description", "description": "Asset description",
@@ -16621,7 +16628,9 @@
}, },
"createdAt": { "createdAt": {
"description": "The UTC timestamp when the asset was originally uploaded to Immich.", "description": "The UTC timestamp when the asset was originally uploaded to Immich.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"duplicateId": { "duplicateId": {
@@ -16639,12 +16648,16 @@
}, },
"fileCreatedAt": { "fileCreatedAt": {
"description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.", "description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"fileModifiedAt": { "fileModifiedAt": {
"description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"hasMetadata": { "hasMetadata": {
@@ -16653,10 +16666,9 @@
}, },
"height": { "height": {
"description": "Asset height", "description": "Asset height",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"id": { "id": {
"description": "Asset ID", "description": "Asset ID",
@@ -16718,7 +16730,9 @@
}, },
"localDateTime": { "localDateTime": {
"description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"originalFileName": { "originalFileName": {
@@ -16791,7 +16805,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"visibility": { "visibility": {
@@ -16799,10 +16815,9 @@
}, },
"width": { "width": {
"description": "Asset width", "description": "Asset width",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17219,27 +17234,23 @@
"properties": { "properties": {
"height": { "height": {
"description": "Height of the crop", "description": "Height of the crop",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"width": { "width": {
"description": "Width of the crop", "description": "Width of the crop",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"x": { "x": {
"description": "Top-Left X coordinate of crop", "description": "Top-Left X coordinate of crop",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
}, },
"y": { "y": {
"description": "Top-Left Y coordinate of crop", "description": "Top-Left Y coordinate of crop",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17263,9 +17274,8 @@
}, },
"keepLastAmount": { "keepLastAmount": {
"description": "Keep last amount", "description": "Keep last amount",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17298,9 +17308,7 @@
}, },
"filesize": { "filesize": {
"description": "Backup file size", "description": "Backup file size",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"timezone": { "timezone": {
"description": "Backup timezone", "description": "Backup timezone",
@@ -17626,8 +17634,10 @@
"dateTimeOriginal": { "dateTimeOriginal": {
"default": null, "default": null,
"description": "Original date/time", "description": "Original date/time",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"description": { "description": {
@@ -17639,18 +17649,16 @@
"exifImageHeight": { "exifImageHeight": {
"default": null, "default": null,
"description": "Image height in pixels", "description": "Image height in pixels",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"exifImageWidth": { "exifImageWidth": {
"default": null, "default": null,
"description": "Image width in pixels", "description": "Image width in pixels",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"exposureTime": { "exposureTime": {
"default": null, "default": null,
@@ -17681,10 +17689,8 @@
"iso": { "iso": {
"default": null, "default": null,
"description": "ISO sensitivity", "description": "ISO sensitivity",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"latitude": { "latitude": {
"default": null, "default": null,
@@ -17719,8 +17725,10 @@
"modifyDate": { "modifyDate": {
"default": null, "default": null,
"description": "Modification date/time", "description": "Modification date/time",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"orientation": { "orientation": {
@@ -17738,10 +17746,8 @@
"rating": { "rating": {
"default": null, "default": null,
"description": "Rating", "description": "Rating",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"default": null, "default": null,
@@ -18168,14 +18174,10 @@
"type": "boolean" "type": "boolean"
}, },
"interval": { "interval": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"timeout": { "timeout": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
} }
}, },
"required": [ "required": [
@@ -18225,9 +18227,7 @@
"properties": { "properties": {
"files": { "files": {
"description": "Number of files in the folder", "description": "Number of files in the folder",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"folder": { "folder": {
"$ref": "#/components/schemas/StorageFolder" "$ref": "#/components/schemas/StorageFolder"
@@ -18270,9 +18270,7 @@
"type": "string" "type": "string"
}, },
"progress": { "progress": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"task": { "task": {
"type": "string" "type": "string"
@@ -18749,9 +18747,8 @@
}, },
"page": { "page": {
"description": "Page number", "description": "Page number",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"personIds": { "personIds": {
"description": "Filter by person IDs", "description": "Filter by person IDs",
@@ -18771,7 +18768,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -18793,7 +18790,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -19272,7 +19269,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {
@@ -19627,8 +19626,10 @@
"properties": { "properties": {
"birthDate": { "birthDate": {
"description": "Person date of birth", "description": "Person date of birth",
"example": "2024-01-01",
"format": "date", "format": "date",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string" "type": "string"
}, },
"color": { "color": {
@@ -19679,7 +19680,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string", "type": "string",
"x-immich-history": [ "x-immich-history": [
{ {
@@ -19756,8 +19759,10 @@
"properties": { "properties": {
"birthDate": { "birthDate": {
"description": "Person date of birth", "description": "Person date of birth",
"example": "2024-01-01",
"format": "date", "format": "date",
"nullable": true, "nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
"type": "string" "type": "string"
}, },
"color": { "color": {
@@ -19814,7 +19819,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string", "type": "string",
"x-immich-history": [ "x-immich-history": [
{ {
@@ -20624,7 +20631,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -20646,7 +20653,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -21464,9 +21471,8 @@
}, },
"duration": { "duration": {
"description": "Session duration in seconds", "description": "Session duration in seconds",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
} }
}, },
"type": "object" "type": "object"
@@ -21980,9 +21986,8 @@
}, },
"page": { "page": {
"description": "Page number", "description": "Page number",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"personIds": { "personIds": {
"description": "Filter by person IDs", "description": "Filter by person IDs",
@@ -22008,7 +22013,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -22030,7 +22035,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -22268,7 +22273,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -24400,10 +24405,9 @@
}, },
"defaultStorageQuota": { "defaultStorageQuota": {
"description": "Default storage quota", "description": "Default storage quota",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"enabled": { "enabled": {
"description": "Enabled", "description": "Enabled",
@@ -24578,7 +24582,7 @@
"description": "SMTP server port", "description": "SMTP server port",
"maximum": 65535, "maximum": 65535,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
}, },
"secure": { "secure": {
"description": "Whether to use secure connection (TLS/SSL)", "description": "Whether to use secure connection (TLS/SSL)",
@@ -24844,7 +24848,9 @@
}, },
"createdAt": { "createdAt": {
"description": "Creation date", "description": "Creation date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"id": { "id": {
@@ -24861,7 +24867,9 @@
}, },
"updatedAt": { "updatedAt": {
"description": "Last update date", "description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"value": { "value": {
@@ -25515,7 +25523,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {
@@ -25797,7 +25807,9 @@
}, },
"profileChangedAt": { "profileChangedAt": {
"description": "Profile change date", "description": "Profile change date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time", "format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"profileImagePath": { "profileImagePath": {
@@ -25996,9 +26008,7 @@
}, },
"order": { "order": {
"description": "Action order", "description": "Action order",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"pluginActionId": { "pluginActionId": {
"description": "Plugin action ID", "description": "Plugin action ID",
@@ -26097,9 +26107,7 @@
}, },
"order": { "order": {
"description": "Filter order", "description": "Filter order",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"pluginFilterId": { "pluginFilterId": {
"description": "Plugin filter ID", "description": "Plugin filter ID",
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "2.7.5", "version": "2.7.5",
"description": "Monorepo for Immich", "description": "Monorepo for Immich",
"private": true, "private": true,
"packageManager": "pnpm@10.33.1+sha512.05ba3c1d5d1c18f68df06470d74055e62d41fc110a0c660db1b2dfb2785327f04cf0f68345d4609bc52089e7fa0343c31593b2f9594e2c5d5da426230acc9820", "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": { "engines": {
"pnpm": ">=10.0.0" "pnpm": ">=10.0.0"
} }
+998 -960
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -46,15 +46,15 @@
"@nestjs/platform-express": "^11.0.4", "@nestjs/platform-express": "^11.0.4",
"@nestjs/platform-socket.io": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4",
"@nestjs/schedule": "^6.0.0", "@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.4.2", "@nestjs/swagger": "11.2.6",
"@nestjs/websockets": "^11.0.4", "@nestjs/websockets": "^11.0.4",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/exporter-prometheus": "^0.215.0", "@opentelemetry/exporter-prometheus": "^0.215.0",
"@opentelemetry/instrumentation-http": "^0.215.0", "@opentelemetry/instrumentation-http": "^0.215.0",
"@opentelemetry/instrumentation-ioredis": "^0.63.0", "@opentelemetry/instrumentation-ioredis": "^0.62.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.61.0", "@opentelemetry/instrumentation-nestjs-core": "^0.60.0",
"@opentelemetry/instrumentation-pg": "^0.67.0", "@opentelemetry/instrumentation-pg": "^0.66.0",
"@opentelemetry/resources": "^2.0.1", "@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1",
"@opentelemetry/sdk-node": "^0.215.0", "@opentelemetry/sdk-node": "^0.215.0",
+4 -4
View File
@@ -42,19 +42,19 @@ import { configureUserAgent } from 'src/utils/fetch';
const common = [...repositories, ...services, GlobalExceptionFilter]; const common = [...repositories, ...services, GlobalExceptionFilter];
const configRepository = new ConfigRepository();
const { bull, cls, database, otel } = configRepository.getEnv();
const commonMiddleware = [ const commonMiddleware = [
{ provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_FILTER, useClass: GlobalExceptionFilter },
{ provide: APP_PIPE, useClass: ZodValidationPipe }, { provide: APP_PIPE, useClass: ZodValidationPipe },
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, ...(configRepository.isDev() ? [{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }] : []),
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
]; ];
const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, { provide: APP_GUARD, useClass: AuthGuard }]; const apiMiddleware = [FileUploadInterceptor, ...commonMiddleware, { provide: APP_GUARD, useClass: AuthGuard }];
const configRepository = new ConfigRepository();
const { bull, cls, database, otel } = configRepository.getEnv();
const commonImports = [ const commonImports = [
ClsModule.forRoot(cls.config), ClsModule.forRoot(cls.config),
KyselyModule.forRoot(getKyselyConfig(database.config)), KyselyModule.forRoot(getKyselyConfig(database.config)),
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AddUsersDto, AddUsersDto,
@@ -27,6 +28,7 @@ export class AlbumController {
@Get() @Get()
@Authenticated({ permission: Permission.AlbumRead }) @Authenticated({ permission: Permission.AlbumRead })
@ZodSerializerDto([AlbumResponseDto])
@Endpoint({ @Endpoint({
summary: 'List all albums', summary: 'List all albums',
description: 'Retrieve a list of albums available to the authenticated user.', description: 'Retrieve a list of albums available to the authenticated user.',
@@ -38,6 +40,7 @@ export class AlbumController {
@Post() @Post()
@Authenticated({ permission: Permission.AlbumCreate }) @Authenticated({ permission: Permission.AlbumCreate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create an album', summary: 'Create an album',
description: 'Create a new album. The album can also be created with initial users and assets.', description: 'Create a new album. The album can also be created with initial users and assets.',
@@ -60,6 +63,7 @@ export class AlbumController {
@Authenticated({ permission: Permission.AlbumRead, sharedLink: true }) @Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
@Get(':id') @Get(':id')
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve an album', summary: 'Retrieve an album',
description: 'Retrieve information about a specific album by its ID.', description: 'Retrieve information about a specific album by its ID.',
@@ -71,6 +75,7 @@ export class AlbumController {
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.AlbumUpdate }) @Authenticated({ permission: Permission.AlbumUpdate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update an album', summary: 'Update an album',
description: description:
@@ -152,6 +157,7 @@ export class AlbumController {
@Put(':id/users') @Put(':id/users')
@Authenticated({ permission: Permission.AlbumUserCreate }) @Authenticated({ permission: Permission.AlbumUserCreate })
@ZodSerializerDto(AlbumResponseDto)
@Endpoint({ @Endpoint({
summary: 'Share album with users', summary: 'Share album with users',
description: 'Share an album with multiple users. Each user can be given a specific role in the album.', description: 'Share an album with multiple users. Each user can be given a specific role in the album.',
@@ -1,7 +1,10 @@
import { AssetController } from 'src/controllers/asset.controller'; import { AssetController } from 'src/controllers/asset.controller';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetMetadataKey } from 'src/enum'; import { AssetMetadataKey } from 'src/enum';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import request from 'supertest'; import request from 'supertest';
import { AssetFactory } from 'test/factories/asset.factory';
import { getForAsset } from 'test/mappers';
import { factory } from 'test/small.factory'; import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
@@ -183,6 +186,10 @@ describe(AssetController.name, () => {
}); });
describe('PUT /assets/:id', () => { describe('PUT /assets/:id', () => {
beforeEach(() => {
service.update.mockResolvedValue(mapAsset(getForAsset(AssetFactory.create())));
});
it('should be an authenticated route', async () => { it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/123`); await request(ctx.getHttpServer()).get(`/assets/123`);
expect(ctx.authenticate).toHaveBeenCalled(); expect(ctx.authenticate).toHaveBeenCalled();
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { import {
@@ -79,6 +80,7 @@ export class AssetController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AssetRead, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ZodSerializerDto(AssetResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve an asset', summary: 'Retrieve an asset',
description: 'Retrieve detailed information about a specific asset.', description: 'Retrieve detailed information about a specific asset.',
@@ -128,6 +130,7 @@ export class AssetController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AssetUpdate }) @Authenticated({ permission: Permission.AssetUpdate })
@ZodSerializerDto(AssetResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update an asset', summary: 'Update an asset',
description: 'Update information of a specific asset.', description: 'Update information of a specific asset.',
@@ -1,7 +1,9 @@
import { AuthController } from 'src/controllers/auth.controller'; import { AuthController } from 'src/controllers/auth.controller';
import { LoginResponseDto } from 'src/dtos/auth.dto'; import { LoginResponseDto } from 'src/dtos/auth.dto';
import { mapUserAdmin } from 'src/dtos/user.dto';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import request from 'supertest'; import request from 'supertest';
import { UserFactory } from 'test/factories/user.factory';
import { mediumFactory } from 'test/medium.factory'; import { mediumFactory } from 'test/medium.factory';
import { errorDto } from 'test/medium/responses'; import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
@@ -53,6 +55,7 @@ describe(AuthController.name, () => {
it('should transform email to lower case', async () => { it('should transform email to lower case', async () => {
service.adminSignUp.mockReset(); service.adminSignUp.mockReset();
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
const { status } = await request(ctx.getHttpServer()) const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
@@ -61,6 +64,7 @@ describe(AuthController.name, () => {
}); });
it('should accept an email with a local domain', async () => { it('should accept an email with a local domain', async () => {
service.adminSignUp.mockResolvedValue(mapUserAdmin(UserFactory.create()));
const { status } = await request(ctx.getHttpServer()) const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'admin@local' }); .send({ name: 'admin', password: 'password', email: 'admin@local' });
@@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AuthDto, AuthDto,
@@ -50,6 +51,7 @@ export class AuthController {
} }
@Post('admin-sign-up') @Post('admin-sign-up')
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Register admin', summary: 'Register admin',
description: 'Create the first admin user in the system.', description: 'Create the first admin user in the system.',
@@ -74,6 +76,7 @@ export class AuthController {
@Post('change-password') @Post('change-password')
@Authenticated({ permission: Permission.AuthChangePassword }) @Authenticated({ permission: Permission.AuthChangePassword })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Change password', summary: 'Change password',
description: 'Change the password of the current user.', description: 'Change the password of the current user.',
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@@ -44,6 +45,7 @@ export class FaceController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.FaceUpdate }) @Authenticated({ permission: Permission.FaceUpdate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Re-assign a face to another person', summary: 'Re-assign a face to another person',
description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.', description: 'Re-assign the face provided in the body to the person identified by the id in the path parameter.',
@@ -1,6 +1,7 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { import {
AuthDto, AuthDto,
@@ -89,6 +90,7 @@ export class OAuthController {
@Post('link') @Post('link')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Link OAuth account', summary: 'Link OAuth account',
description: 'Link an OAuth account to the authenticated user.', description: 'Link an OAuth account to the authenticated user.',
@@ -105,6 +107,7 @@ export class OAuthController {
@Post('unlink') @Post('unlink')
@Authenticated() @Authenticated()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Unlink OAuth account', summary: 'Unlink OAuth account',
description: 'Unlink the OAuth account from the authenticated user.', description: 'Unlink the OAuth account from the authenticated user.',
@@ -14,6 +14,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -58,6 +59,7 @@ export class PersonController {
@Post() @Post()
@Authenticated({ permission: Permission.PersonCreate }) @Authenticated({ permission: Permission.PersonCreate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a person', summary: 'Create a person',
description: 'Create a new person that can have multiple faces assigned to them.', description: 'Create a new person that can have multiple faces assigned to them.',
@@ -92,6 +94,7 @@ export class PersonController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Get a person', summary: 'Get a person',
description: 'Retrieve a person by id.', description: 'Retrieve a person by id.',
@@ -103,6 +106,7 @@ export class PersonController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PersonUpdate }) @Authenticated({ permission: Permission.PersonUpdate })
@ZodSerializerDto(PersonResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update person', summary: 'Update person',
description: 'Update an individual person.', description: 'Update an individual person.',
@@ -158,6 +162,7 @@ export class PersonController {
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated({ permission: Permission.PersonReassign }) @Authenticated({ permission: Permission.PersonReassign })
@ZodSerializerDto([PersonResponseDto])
@Endpoint({ @Endpoint({
summary: 'Reassign faces', summary: 'Reassign faces',
description: 'Bulk reassign a list of faces to a different person.', description: 'Bulk reassign a list of faces to a different person.',
@@ -49,7 +49,7 @@ describe(SearchController.name, () => {
}); });
it('should reject an invalid size', async () => { it('should reject an invalid size', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1 }); const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['[size] Too small: expected number to be >=1'])); expect(body).toEqual(errorDto.badRequest(['[size] Too small: expected number to be >=1']));
}); });
@@ -1,5 +1,6 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -54,6 +55,7 @@ export class SearchController {
@Post('random') @Post('random')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search random assets', summary: 'Search random assets',
description: 'Retrieve a random selection of assets based on the provided criteria.', description: 'Retrieve a random selection of assets based on the provided criteria.',
@@ -66,6 +68,7 @@ export class SearchController {
@Post('large-assets') @Post('large-assets')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search large assets', summary: 'Search large assets',
description: 'Search for assets that are considered large based on specified criteria.', description: 'Search for assets that are considered large based on specified criteria.',
@@ -100,6 +103,7 @@ export class SearchController {
@Get('person') @Get('person')
@Authenticated({ permission: Permission.PersonRead }) @Authenticated({ permission: Permission.PersonRead })
@ZodSerializerDto([PersonResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search people', summary: 'Search people',
description: 'Search for people by name.', description: 'Search for people by name.',
@@ -122,6 +126,7 @@ export class SearchController {
@Get('cities') @Get('cities')
@Authenticated({ permission: Permission.AssetRead }) @Authenticated({ permission: Permission.AssetRead })
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve assets by city', summary: 'Retrieve assets by city',
description: description:
+6
View File
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -23,6 +24,7 @@ export class TagController {
@Post() @Post()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a tag', summary: 'Create a tag',
description: 'Create a new tag by providing a name and optional color.', description: 'Create a new tag by providing a name and optional color.',
@@ -34,6 +36,7 @@ export class TagController {
@Get() @Get()
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ZodSerializerDto([TagResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve tags', summary: 'Retrieve tags',
description: 'Retrieve a list of all tags.', description: 'Retrieve a list of all tags.',
@@ -45,6 +48,7 @@ export class TagController {
@Put() @Put()
@Authenticated({ permission: Permission.TagCreate }) @Authenticated({ permission: Permission.TagCreate })
@ZodSerializerDto([TagResponseDto])
@Endpoint({ @Endpoint({
summary: 'Upsert tags', summary: 'Upsert tags',
description: 'Create or update multiple tags in a single request.', description: 'Create or update multiple tags in a single request.',
@@ -67,6 +71,7 @@ export class TagController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.TagRead }) @Authenticated({ permission: Permission.TagRead })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a tag', summary: 'Retrieve a tag',
description: 'Retrieve a specific tag by its ID.', description: 'Retrieve a specific tag by its ID.',
@@ -78,6 +83,7 @@ export class TagController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.TagUpdate }) @Authenticated({ permission: Permission.TagUpdate })
@ZodSerializerDto(TagResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update a tag', summary: 'Update a tag',
description: 'Update an existing tag identified by its ID.', description: 'Update an existing tag identified by its ID.',
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -24,6 +25,7 @@ export class UserAdminController {
@Get() @Get()
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ZodSerializerDto([UserAdminResponseDto])
@Endpoint({ @Endpoint({
summary: 'Search users', summary: 'Search users',
description: 'Search for users.', description: 'Search for users.',
@@ -35,6 +37,7 @@ export class UserAdminController {
@Post() @Post()
@Authenticated({ permission: Permission.AdminUserCreate, admin: true }) @Authenticated({ permission: Permission.AdminUserCreate, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Create a user', summary: 'Create a user',
description: 'Create a new user.', description: 'Create a new user.',
@@ -46,6 +49,7 @@ export class UserAdminController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a user', summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.', description: 'Retrieve a specific user by their ID.',
@@ -57,6 +61,7 @@ export class UserAdminController {
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update a user', summary: 'Update a user',
description: 'Update an existing user.', description: 'Update an existing user.',
@@ -72,6 +77,7 @@ export class UserAdminController {
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Delete a user', summary: 'Delete a user',
description: 'Delete a user.', description: 'Delete a user.',
@@ -140,6 +146,7 @@ export class UserAdminController {
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ permission: Permission.AdminUserDelete, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Restore a deleted user', summary: 'Restore a deleted user',
description: 'Restore a previously deleted user.', description: 'Restore a previously deleted user.',
@@ -15,6 +15,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@@ -40,6 +41,7 @@ export class UserController {
@Get() @Get()
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto([UserResponseDto])
@Endpoint({ @Endpoint({
summary: 'Get all users', summary: 'Get all users',
description: 'Retrieve a list of all users on the server.', description: 'Retrieve a list of all users on the server.',
@@ -51,6 +53,7 @@ export class UserController {
@Get('me') @Get('me')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Get current user', summary: 'Get current user',
description: 'Retrieve information about the user making the API request.', description: 'Retrieve information about the user making the API request.',
@@ -62,6 +65,7 @@ export class UserController {
@Put('me') @Put('me')
@Authenticated({ permission: Permission.UserUpdate }) @Authenticated({ permission: Permission.UserUpdate })
@ZodSerializerDto(UserAdminResponseDto)
@Endpoint({ @Endpoint({
summary: 'Update current user', summary: 'Update current user',
description: 'Update the current user making the API request.', description: 'Update the current user making the API request.',
@@ -166,6 +170,7 @@ export class UserController {
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.UserRead }) @Authenticated({ permission: Permission.UserRead })
@ZodSerializerDto(UserResponseDto)
@Endpoint({ @Endpoint({
summary: 'Retrieve a user', summary: 'Retrieve a user',
description: 'Retrieve a specific user by their ID.', description: 'Retrieve a specific user by their ID.',
@@ -1,5 +1,6 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ZodSerializerDto } from 'nestjs-zod';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -25,6 +26,7 @@ export class ViewController {
@Get('folder') @Get('folder')
@Authenticated({ permission: Permission.FolderRead }) @Authenticated({ permission: Permission.FolderRead })
@ZodSerializerDto([AssetResponseDto])
@Endpoint({ @Endpoint({
summary: 'Retrieve assets by original path', summary: 'Retrieve assets by original path',
description: 'Retrieve assets that are children of a specific folder.', description: 'Retrieve assets that are children of a specific folder.',
+2 -2
View File
@@ -11,8 +11,8 @@ describe('mapAlbum', () => {
.asset({ localDateTime: startDate }, (builder) => builder.exif()) .asset({ localDateTime: startDate }, (builder) => builder.exif())
.build(); .build();
const dto = mapAlbum(getForAlbum(album)); const dto = mapAlbum(getForAlbum(album));
expect(dto.startDate).toEqual(startDate.toISOString()); expect(dto.startDate).toEqual(startDate);
expect(dto.endDate).toEqual(endDate.toISOString()); expect(dto.endDate).toEqual(endDate);
}); });
it('should not set start and end dates for empty assets', () => { it('should not set start and end dates for empty assets', () => {
+11 -21
View File
@@ -6,8 +6,7 @@ import { MapAsset } from 'src/dtos/asset-response.dto';
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { isoDatetimeToDate, stringToBool } from 'src/validation';
import { stringToBool } from 'src/validation';
import z from 'zod'; import z from 'zod';
const AlbumUserAddSchema = z const AlbumUserAddSchema = z
@@ -105,10 +104,8 @@ export const AlbumResponseSchema = z
id: z.string().describe('Album ID'), id: z.string().describe('Album ID'),
albumName: z.string().describe('Album name'), albumName: z.string().describe('Album name'),
description: z.string().describe('Album description'), description: z.string().describe('Album description'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('Creation date'),
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), updatedAt: isoDatetimeToDate.describe('Last update date'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'),
shared: z.boolean().describe('Is shared album'), shared: z.boolean().describe('Is shared album'),
albumUsers: z albumUsers: z
@@ -119,16 +116,9 @@ export const AlbumResponseSchema = z
), ),
hasSharedLink: z.boolean().describe('Has shared link'), hasSharedLink: z.boolean().describe('Has shared link'),
assetCount: z.int().min(0).describe('Number of assets'), assetCount: z.int().min(0).describe('Number of assets'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. lastModifiedAssetTimestamp: isoDatetimeToDate.optional().describe('Last modified asset timestamp'),
lastModifiedAssetTimestamp: z startDate: isoDatetimeToDate.optional().describe('Start date (earliest asset)'),
.string() endDate: isoDatetimeToDate.optional().describe('End date (latest asset)'),
.meta({ format: 'date-time' })
.optional()
.describe('Last modified asset timestamp'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'),
isActivityEnabled: z.boolean().describe('Activity feed enabled'), isActivityEnabled: z.boolean().describe('Activity feed enabled'),
order: AssetOrderSchema.optional(), order: AssetOrderSchema.optional(),
contributorCounts: z.array(ContributorCountResponseSchema).optional(), contributorCounts: z.array(ContributorCountResponseSchema).optional(),
@@ -144,7 +134,7 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} export class AlbumResponseDto extends createZodDto(AlbumResponseSchema, { codec: true }) {}
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
export type MapAlbumDto = { export type MapAlbumDto = {
@@ -190,14 +180,14 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
albumName: entity.albumName, albumName: entity.albumName,
description: entity.description, description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId, albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
id: entity.id, id: entity.id,
albumUsers, albumUsers,
shared: hasSharedUser || hasSharedLink, shared: hasSharedUser || hasSharedLink,
hasSharedLink, hasSharedLink,
startDate: asDateString(startDate), startDate: startDate ? new Date(startDate) : undefined,
endDate: asDateString(endDate), endDate: endDate ? new Date(endDate) : undefined,
assetCount: entity.assets?.length || 0, assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled, isActivityEnabled: entity.isActivityEnabled,
order: entity.order, order: entity.order,
+24 -42
View File
@@ -25,8 +25,8 @@ import {
import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { getDimensions } from 'src/utils/asset.util'; import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { asDateString } from 'src/utils/date';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { isoDatetimeToDate } from 'src/validation';
import z from 'zod'; import z from 'zod';
const SanitizedAssetResponseSchema = z const SanitizedAssetResponseSchema = z
@@ -40,22 +40,18 @@ const SanitizedAssetResponseSchema = z
) )
.nullable(), .nullable(),
originalMimeType: z.string().optional().describe('Original MIME type'), originalMimeType: z.string().optional().describe('Original MIME type'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. localDateTime: isoDatetimeToDate.describe(
localDateTime: z 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
.string() ),
.meta({ format: 'date-time' })
.describe(
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
),
duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'), duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'),
livePhotoVideoId: z.string().nullish().describe('Live photo video ID'), livePhotoVideoId: z.string().nullish().describe('Live photo video ID'),
hasMetadata: z.boolean().describe('Whether asset has metadata'), hasMetadata: z.boolean().describe('Whether asset has metadata'),
width: z.int().min(0).nullable().describe('Asset width'), width: z.number().min(0).nullable().describe('Asset width'),
height: z.int().min(0).nullable().describe('Asset height'), height: z.number().min(0).nullable().describe('Asset height'),
}) })
.meta({ id: 'SanitizedAssetResponseDto' }); .meta({ id: 'SanitizedAssetResponseDto' });
export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema, { codec: true }) {}
const AssetStackResponseSchema = z const AssetStackResponseSchema = z
.object({ .object({
@@ -67,11 +63,7 @@ const AssetStackResponseSchema = z
export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
z.object({ z.object({
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
createdAt: z
.string()
.meta({ format: 'date-time' })
.describe('The UTC timestamp when the asset was originally uploaded to Immich.'),
ownerId: z.string().describe('Owner user ID'), ownerId: z.string().describe('Owner user ID'),
owner: UserResponseSchema.optional(), owner: UserResponseSchema.optional(),
libraryId: z libraryId: z
@@ -81,25 +73,15 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
.meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()), .meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()),
originalPath: z.string().describe('Original file path'), originalPath: z.string().describe('Original file path'),
originalFileName: z.string().describe('Original file name'), originalFileName: z.string().describe('Original file name'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. fileCreatedAt: isoDatetimeToDate.describe(
fileCreatedAt: z 'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.',
.string() ),
.meta({ format: 'date-time' }) fileModifiedAt: isoDatetimeToDate.describe(
.describe( 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.', ),
), updatedAt: isoDatetimeToDate.describe(
fileModifiedAt: z 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
.string() ),
.meta({ format: 'date-time' })
.describe(
'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.',
),
updatedAt: z
.string()
.meta({ format: 'date-time' })
.describe(
'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.',
),
isFavorite: z.boolean().describe('Is favorite'), isFavorite: z.boolean().describe('Is favorite'),
isArchived: z.boolean().describe('Is archived'), isArchived: z.boolean().describe('Is archived'),
isTrashed: z.boolean().describe('Is trashed'), isTrashed: z.boolean().describe('Is trashed'),
@@ -124,7 +106,7 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
}).shape, }).shape,
).meta({ id: 'AssetResponseDto' }); ).meta({ id: 'AssetResponseDto' });
export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export class AssetResponseDto extends createZodDto(AssetResponseSchema, { codec: true }) {}
export type MapAsset = { export type MapAsset = {
createdAt: Date; createdAt: Date;
@@ -220,7 +202,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
type: entity.type, type: entity.type,
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
localDateTime: asDateString(entity.localDateTime), localDateTime: new Date(entity.localDateTime),
duration: entity.duration, duration: entity.duration,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false, hasMetadata: false,
@@ -234,7 +216,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
return { return {
id: entity.id, id: entity.id,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
ownerId: entity.ownerId, ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined, owner: entity.owner ? mapUser(entity.owner) : undefined,
libraryId: entity.libraryId, libraryId: entity.libraryId,
@@ -243,10 +225,10 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
originalMimeType: mimeTypes.lookup(entity.originalFileName), originalMimeType: mimeTypes.lookup(entity.originalFileName),
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null, thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
fileCreatedAt: asDateString(entity.fileCreatedAt), fileCreatedAt: new Date(entity.fileCreatedAt),
fileModifiedAt: asDateString(entity.fileModifiedAt), fileModifiedAt: new Date(entity.fileModifiedAt),
localDateTime: asDateString(entity.localDateTime), localDateTime: new Date(entity.localDateTime),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite, isFavorite: options.auth?.user.id === entity.ownerId && entity.isFavorite,
isArchived: entity.visibility === AssetVisibility.Archive, isArchived: entity.visibility === AssetVisibility.Archive,
isTrashed: !!entity.deletedAt, isTrashed: !!entity.deletedAt,
+1 -1
View File
@@ -40,7 +40,7 @@ const UpdateAssetBaseSchema = z
const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({ const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({
ids: z.array(z.uuidv4()).describe('Asset IDs to update'), ids: z.array(z.uuidv4()).describe('Asset IDs to update'),
duplicateId: z.string().nullish().describe('Duplicate ID'), duplicateId: z.string().nullish().describe('Duplicate ID'),
dateTimeRelative: z.int().optional().describe('Relative time offset in seconds'), dateTimeRelative: z.number().optional().describe('Relative time offset in seconds'),
timeZone: z.string().optional().describe('Time zone (IANA timezone)'), timeZone: z.string().optional().describe('Time zone (IANA timezone)'),
}); });
+4 -1
View File
@@ -21,7 +21,10 @@ export type AuthDto = {
const LoginCredentialSchema = z const LoginCredentialSchema = z
.object({ .object({
email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), email: toEmail
.transform((val) => val.toLowerCase())
.describe('User email')
.meta({ example: 'testuser@email.com' }),
password: z.string().describe('User password').meta({ example: 'password' }), password: z.string().describe('User password').meta({ example: 'password' }),
}) })
.meta({ id: 'LoginCredentialDto' }); .meta({ id: 'LoginCredentialDto' });
+1 -1
View File
@@ -4,7 +4,7 @@ import z from 'zod';
const DatabaseBackupSchema = z const DatabaseBackupSchema = z
.object({ .object({
filename: z.string().describe('Backup filename'), filename: z.string().describe('Backup filename'),
filesize: z.int().describe('Backup file size'), filesize: z.number().describe('Backup file size'),
timezone: z.string().describe('Backup timezone'), timezone: z.string().describe('Backup timezone'),
}) })
.meta({ id: 'DatabaseBackupDto' }); .meta({ id: 'DatabaseBackupDto' });
+4 -4
View File
@@ -21,10 +21,10 @@ const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mi
const CropParametersSchema = z const CropParametersSchema = z
.object({ .object({
x: z.int().min(0).describe('Top-Left X coordinate of crop'), x: z.number().min(0).describe('Top-Left X coordinate of crop'),
y: z.int().min(0).describe('Top-Left Y coordinate of crop'), y: z.number().min(0).describe('Top-Left Y coordinate of crop'),
width: z.int().min(1).describe('Width of the crop'), width: z.number().min(1).describe('Width of the crop'),
height: z.int().min(1).describe('Height of the crop'), height: z.number().min(1).describe('Height of the crop'),
}) })
.meta({ id: 'CropParameters' }); .meta({ id: 'CropParameters' });
+10 -12
View File
@@ -1,26 +1,24 @@
import { createZodDto } from 'nestjs-zod'; import { createZodDto } from 'nestjs-zod';
import { Exif } from 'src/database'; import { Exif } from 'src/database';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { isoDatetimeToDate } from 'src/validation';
import z from 'zod'; import z from 'zod';
export const ExifResponseSchema = z export const ExifResponseSchema = z
.object({ .object({
make: z.string().nullish().default(null).describe('Camera make'), make: z.string().nullish().default(null).describe('Camera make'),
model: z.string().nullish().default(null).describe('Camera model'), model: z.string().nullish().default(null).describe('Camera model'),
exifImageWidth: z.int().min(0).nullish().default(null).describe('Image width in pixels'), exifImageWidth: z.number().min(0).nullish().default(null).describe('Image width in pixels'),
exifImageHeight: z.int().min(0).nullish().default(null).describe('Image height in pixels'), exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'),
fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'), fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'),
orientation: z.string().nullish().default(null).describe('Image orientation'), orientation: z.string().nullish().default(null).describe('Image orientation'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. dateTimeOriginal: isoDatetimeToDate.nullish().default(null).describe('Original date/time'),
dateTimeOriginal: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Original date/time'), modifyDate: isoDatetimeToDate.nullish().default(null).describe('Modification date/time'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'),
timeZone: z.string().nullish().default(null).describe('Time zone'), timeZone: z.string().nullish().default(null).describe('Time zone'),
lensModel: z.string().nullish().default(null).describe('Lens model'), lensModel: z.string().nullish().default(null).describe('Lens model'),
fNumber: z.number().nullish().default(null).describe('F-number (aperture)'), fNumber: z.number().nullish().default(null).describe('F-number (aperture)'),
focalLength: z.number().nullish().default(null).describe('Focal length in mm'), focalLength: z.number().nullish().default(null).describe('Focal length in mm'),
iso: z.int().nullish().default(null).describe('ISO sensitivity'), iso: z.number().nullish().default(null).describe('ISO sensitivity'),
exposureTime: z.string().nullish().default(null).describe('Exposure time'), exposureTime: z.string().nullish().default(null).describe('Exposure time'),
latitude: z.number().nullish().default(null).describe('GPS latitude'), latitude: z.number().nullish().default(null).describe('GPS latitude'),
longitude: z.number().nullish().default(null).describe('GPS longitude'), longitude: z.number().nullish().default(null).describe('GPS longitude'),
@@ -29,12 +27,12 @@ export const ExifResponseSchema = z
country: z.string().nullish().default(null).describe('Country name'), country: z.string().nullish().default(null).describe('Country name'),
description: z.string().nullish().default(null).describe('Image description'), description: z.string().nullish().default(null).describe('Image description'),
projectionType: z.string().nullish().default(null).describe('Projection type'), projectionType: z.string().nullish().default(null).describe('Projection type'),
rating: z.int().nullish().default(null).describe('Rating'), rating: z.number().nullish().default(null).describe('Rating'),
}) })
.describe('EXIF response') .describe('EXIF response')
.meta({ id: 'ExifResponseDto' }); .meta({ id: 'ExifResponseDto' });
class ExifResponseDto extends createZodDto(ExifResponseSchema) {} class ExifResponseDto extends createZodDto(ExifResponseSchema, { codec: true }) {}
export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto { export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
return { return {
@@ -44,8 +42,8 @@ export function mapExif(entity: MaybeDehydrated<Exif>): ExifResponseDto {
exifImageHeight: entity.exifImageHeight, exifImageHeight: entity.exifImageHeight,
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
orientation: entity.orientation, orientation: entity.orientation,
dateTimeOriginal: asDateString(entity.dateTimeOriginal), dateTimeOriginal: entity.dateTimeOriginal ? new Date(entity.dateTimeOriginal) : null,
modifyDate: asDateString(entity.modifyDate), modifyDate: entity.modifyDate ? new Date(entity.modifyDate) : null,
timeZone: entity.timeZone, timeZone: entity.timeZone,
lensModel: entity.lensModel, lensModel: entity.lensModel,
fNumber: entity.fNumber, fNumber: entity.fNumber,
+2 -2
View File
@@ -29,7 +29,7 @@ const MaintenanceStatusResponseSchema = z
.object({ .object({
active: z.boolean(), active: z.boolean(),
action: MaintenanceActionSchema, action: MaintenanceActionSchema,
progress: z.int().optional(), progress: z.number().optional(),
task: z.string().optional(), task: z.string().optional(),
error: z.string().optional(), error: z.string().optional(),
}) })
@@ -40,7 +40,7 @@ const MaintenanceDetectInstallStorageFolderSchema = z
folder: StorageFolderSchema, folder: StorageFolderSchema,
readable: z.boolean().describe('Whether the folder is readable'), readable: z.boolean().describe('Whether the folder is readable'),
writable: z.boolean().describe('Whether the folder is writable'), writable: z.boolean().describe('Whether the folder is writable'),
files: z.int().describe('Number of files in the folder'), files: z.number().describe('Number of files in the folder'),
}) })
.meta({ id: 'MaintenanceDetectInstallStorageFolderDto' }); .meta({ id: 'MaintenanceDetectInstallStorageFolderDto' });
+9 -14
View File
@@ -7,9 +7,8 @@ import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { SourceTypeSchema } from 'src/enum'; import { SourceTypeSchema } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { ImageDimensions, MaybeDehydrated } from 'src/types';
import { asBirthDateString, asDateString } from 'src/utils/date';
import { transformFaceBoundingBox } from 'src/utils/transform'; import { transformFaceBoundingBox } from 'src/utils/transform';
import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; import { emptyStringToNull, hexColor, isoDateToDate, isoDatetimeToDate, stringToBool } from 'src/validation';
import z from 'zod'; import z from 'zod';
const PersonCreateSchema = z const PersonCreateSchema = z
@@ -51,8 +50,8 @@ const PersonSearchSchema = z
withHidden: stringToBool.optional().describe('Include hidden people'), withHidden: stringToBool.optional().describe('Include hidden people'),
closestPersonId: z.uuidv4().optional().describe('Closest person ID for similarity search'), closestPersonId: z.uuidv4().optional().describe('Closest person ID for similarity search'),
closestAssetId: z.uuidv4().optional().describe('Closest asset ID for similarity search'), closestAssetId: z.uuidv4().optional().describe('Closest asset ID for similarity search'),
page: z.coerce.number().int().min(1).default(1).describe('Page number for pagination'), page: z.coerce.number().min(1).default(1).describe('Page number for pagination'),
size: z.coerce.number().int().min(1).max(1000).default(500).describe('Number of items per page'), size: z.coerce.number().min(1).max(1000).default(500).describe('Number of items per page'),
}) })
.meta({ id: 'PersonSearchDto' }); .meta({ id: 'PersonSearchDto' });
@@ -60,14 +59,10 @@ const PersonResponseSchema = z
.object({ .object({
id: z.string().describe('Person ID'), id: z.string().describe('Person ID'),
name: z.string().describe('Person name'), name: z.string().describe('Person name'),
// TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers. birthDate: isoDateToDate.nullable().describe('Person date of birth'),
birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(),
thumbnailPath: z.string().describe('Thumbnail path'), thumbnailPath: z.string().describe('Thumbnail path'),
isHidden: z.boolean().describe('Is hidden'), isHidden: z.boolean().describe('Is hidden'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. updatedAt: isoDatetimeToDate
updatedAt: z
.string()
.meta({ format: 'date-time' })
.optional() .optional()
.describe('Last update date') .describe('Last update date')
.meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()), .meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()),
@@ -89,7 +84,7 @@ export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
export class MergePersonDto extends createZodDto(MergePersonSchema) {} export class MergePersonDto extends createZodDto(MergePersonSchema) {}
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} export class PersonResponseDto extends createZodDto(PersonResponseSchema, { codec: true }) {}
export const AssetFaceWithoutPersonResponseSchema = z export const AssetFaceWithoutPersonResponseSchema = z
.object({ .object({
@@ -111,7 +106,7 @@ export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({
faces: z.array(AssetFaceWithoutPersonResponseSchema), faces: z.array(AssetFaceWithoutPersonResponseSchema),
}).meta({ id: 'PersonWithFacesResponseDto' }); }).meta({ id: 'PersonWithFacesResponseDto' });
export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema, { codec: true }) {}
const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({
person: PersonResponseSchema.nullable(), person: PersonResponseSchema.nullable(),
@@ -184,12 +179,12 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
return { return {
id: person.id, id: person.id,
name: person.name, name: person.name,
birthDate: asBirthDateString(person.birthDate), birthDate: person.birthDate ? new Date(person.birthDate) : null,
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: person.color ?? undefined, color: person.color ?? undefined,
updatedAt: asDateString(person.updatedAt), updatedAt: person.updatedAt ? new Date(person.updatedAt) : undefined,
}; };
} }
+5 -5
View File
@@ -34,7 +34,7 @@ const BaseSearchSchema = z.object({
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'), tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'),
albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'), albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'),
rating: z rating: z
.int() .number()
.min(-1) .min(-1)
.max(5) .max(5)
.nullish() .nullish()
@@ -52,7 +52,7 @@ const BaseSearchSchema = z.object({
const BaseSearchWithResultsSchema = BaseSearchSchema.extend({ const BaseSearchWithResultsSchema = BaseSearchSchema.extend({
withDeleted: z.boolean().optional().describe('Include deleted assets'), withDeleted: z.boolean().optional().describe('Include deleted assets'),
withExif: z.boolean().optional().describe('Include EXIF data in response'), withExif: z.boolean().optional().describe('Include EXIF data in response'),
size: z.int().min(1).max(1000).optional().describe('Number of results to return'), size: z.number().min(1).max(1000).optional().describe('Number of results to return'),
}); });
const RandomSearchSchema = BaseSearchWithResultsSchema.extend({ const RandomSearchSchema = BaseSearchWithResultsSchema.extend({
@@ -62,7 +62,7 @@ const RandomSearchSchema = BaseSearchWithResultsSchema.extend({
const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({
minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'), minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'),
size: z.coerce.number().int().min(1).max(1000).optional().describe('Number of results to return'), size: z.coerce.number().min(1).max(1000).optional().describe('Number of results to return'),
}).meta({ id: 'LargeAssetSearchDto' }); }).meta({ id: 'LargeAssetSearchDto' });
const MetadataSearchSchema = RandomSearchSchema.extend({ const MetadataSearchSchema = RandomSearchSchema.extend({
@@ -75,7 +75,7 @@ const MetadataSearchSchema = RandomSearchSchema.extend({
thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'), thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'),
encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'), encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'),
order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'), order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'),
page: z.int().min(1).optional().describe('Page number'), page: z.number().min(1).optional().describe('Page number'),
}).meta({ id: 'MetadataSearchDto' }); }).meta({ id: 'MetadataSearchDto' });
const StatisticsSearchSchema = BaseSearchSchema.extend({ const StatisticsSearchSchema = BaseSearchSchema.extend({
@@ -86,7 +86,7 @@ const SmartSearchSchema = BaseSearchWithResultsSchema.extend({
query: z.string().trim().optional().describe('Natural language search query'), query: z.string().trim().optional().describe('Natural language search query'),
queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'), queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'),
language: z.string().optional().describe('Search language code'), language: z.string().optional().describe('Search language code'),
page: z.int().min(1).optional().describe('Page number'), page: z.number().min(1).optional().describe('Page number'),
}).meta({ id: 'SmartSearchDto' }); }).meta({ id: 'SmartSearchDto' });
const SearchPlacesSchema = z const SearchPlacesSchema = z
+1 -1
View File
@@ -4,7 +4,7 @@ import z from 'zod';
const SessionCreateSchema = z const SessionCreateSchema = z
.object({ .object({
duration: z.int().min(1).optional().describe('Session duration in seconds'), duration: z.number().min(1).optional().describe('Session duration in seconds'),
deviceType: z.string().optional().describe('Device type'), deviceType: z.string().optional().describe('Device type'),
deviceOS: z.string().optional().describe('Device OS'), deviceOS: z.string().optional().describe('Device OS'),
}) })
+5 -5
View File
@@ -51,7 +51,7 @@ const DatabaseBackupSchema = z
.object({ .object({
enabled: configBool.describe('Enabled'), enabled: configBool.describe('Enabled'),
cronExpression: cronExpressionSchema, cronExpression: cronExpressionSchema,
keepLastAmount: z.int().min(1).describe('Keep last amount'), keepLastAmount: z.number().min(1).describe('Keep last amount'),
}) })
.meta({ id: 'DatabaseBackupConfig' }); .meta({ id: 'DatabaseBackupConfig' });
@@ -130,8 +130,8 @@ const SystemConfigLoggingSchema = z
const MachineLearningAvailabilityChecksSchema = z const MachineLearningAvailabilityChecksSchema = z
.object({ .object({
enabled: configBool.describe('Enabled'), enabled: configBool.describe('Enabled'),
timeout: z.int(), timeout: z.number(),
interval: z.int(), interval: z.number(),
}) })
.meta({ id: 'MachineLearningAvailabilityChecksDto' }); .meta({ id: 'MachineLearningAvailabilityChecksDto' });
@@ -180,7 +180,7 @@ const SystemConfigOAuthSchema = z
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema,
timeout: z.int().min(1).describe('Timeout'), timeout: z.int().min(1).describe('Timeout'),
allowInsecureRequests: configBool.describe('Allow insecure requests'), allowInsecureRequests: configBool.describe('Allow insecure requests'),
defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), defaultStorageQuota: z.number().min(0).nullable().describe('Default storage quota'),
enabled: configBool.describe('Enabled'), enabled: configBool.describe('Enabled'),
issuerUrl: z issuerUrl: z
.string() .string()
@@ -254,7 +254,7 @@ const SystemConfigSmtpTransportSchema = z
.object({ .object({
ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'),
host: z.string().describe('SMTP server hostname'), host: z.string().describe('SMTP server hostname'),
port: z.int().min(0).max(65_535).describe('SMTP server port'), port: z.number().min(0).max(65_535).describe('SMTP server port'),
secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), secure: configBool.describe('Whether to use secure connection (TLS/SSL)'),
username: z.string().describe('SMTP username'), username: z.string().describe('SMTP username'),
password: z.string().describe('SMTP password'), password: z.string().describe('SMTP password'),
+6 -9
View File
@@ -1,8 +1,7 @@
import { createZodDto } from 'nestjs-zod'; import { createZodDto } from 'nestjs-zod';
import { Tag } from 'src/database'; import { Tag } from 'src/database';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { emptyStringToNull, hexColor, isoDatetimeToDate } from 'src/validation';
import { emptyStringToNull, hexColor } from 'src/validation';
import z from 'zod'; import z from 'zod';
const TagCreateSchema = z const TagCreateSchema = z
@@ -44,10 +43,8 @@ export const TagResponseSchema = z
parentId: z.string().optional().describe('Parent tag ID'), parentId: z.string().optional().describe('Parent tag ID'),
name: z.string().describe('Tag name'), name: z.string().describe('Tag name'),
value: z.string().describe('Tag value (full path)'), value: z.string().describe('Tag value (full path)'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: isoDatetimeToDate.describe('Creation date'),
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), updatedAt: isoDatetimeToDate.describe('Last update date'),
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'),
color: z.string().optional().describe('Tag color (hex)'), color: z.string().optional().describe('Tag color (hex)'),
}) })
.meta({ id: 'TagResponseDto' }); .meta({ id: 'TagResponseDto' });
@@ -57,7 +54,7 @@ export class TagUpdateDto extends createZodDto(TagUpdateSchema) {}
export class TagUpsertDto extends createZodDto(TagUpsertSchema) {} export class TagUpsertDto extends createZodDto(TagUpsertSchema) {}
export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {} export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {}
export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {} export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {}
export class TagResponseDto extends createZodDto(TagResponseSchema) {} export class TagResponseDto extends createZodDto(TagResponseSchema, { codec: true }) {}
export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto { export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
return { return {
@@ -65,8 +62,8 @@ export function mapTag(entity: MaybeDehydrated<Tag>): TagResponseDto {
parentId: entity.parentId ?? undefined, parentId: entity.parentId ?? undefined,
name: entity.value.split('/').at(-1) as string, name: entity.value.split('/').at(-1) as string,
value: entity.value, value: entity.value,
createdAt: asDateString(entity.createdAt), createdAt: new Date(entity.createdAt),
updatedAt: asDateString(entity.updatedAt), updatedAt: new Date(entity.updatedAt),
color: entity.color ?? undefined, color: entity.color ?? undefined,
}; };
} }
+13 -9
View File
@@ -3,13 +3,15 @@ import { User, UserAdmin } from 'src/database';
import { pinCodeRegex } from 'src/dtos/auth.dto'; import { pinCodeRegex } from 'src/dtos/auth.dto';
import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum';
import { MaybeDehydrated, UserMetadataItem } from 'src/types'; import { MaybeDehydrated, UserMetadataItem } from 'src/types';
import { asDateString } from 'src/utils/date';
import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
import z from 'zod'; import z from 'zod';
export const UserUpdateMeSchema = z export const UserUpdateMeSchema = z
.object({ .object({
email: toEmail.optional().describe('User email'), email: toEmail
.transform((val) => val.toLowerCase())
.optional()
.describe('User email'),
password: z password: z
.string() .string()
.optional() .optional()
@@ -29,12 +31,11 @@ export const UserResponseSchema = z
email: toEmail.describe('User email'), email: toEmail.describe('User email'),
profileImagePath: z.string().describe('Profile image path'), profileImagePath: z.string().describe('Profile image path'),
avatarColor: UserAvatarColorSchema, avatarColor: UserAvatarColorSchema,
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. profileChangedAt: isoDatetimeToDate.describe('Profile change date'),
profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'),
}) })
.meta({ id: 'UserResponseDto' }); .meta({ id: 'UserResponseDto' });
export class UserResponseDto extends createZodDto(UserResponseSchema) {} export class UserResponseDto extends createZodDto(UserResponseSchema, { codec: true }) {}
const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/;
@@ -61,7 +62,7 @@ export const mapUser = (entity: MaybeDehydrated<User | UserAdmin>): UserResponse
name: entity.name, name: entity.name,
profileImagePath: entity.profileImagePath, profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email), avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: asDateString(entity.profileChangedAt), profileChangedAt: new Date(entity.profileChangedAt),
}; };
}; };
@@ -76,7 +77,7 @@ export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {}
export const UserAdminCreateSchema = z export const UserAdminCreateSchema = z
.object({ .object({
email: toEmail.describe('User email'), email: toEmail.transform((val) => val.toLowerCase()).describe('User email'),
password: z.string().describe('User password'), password: z.string().describe('User password'),
name: z.string().describe('User name'), name: z.string().describe('User name'),
avatarColor: UserAvatarColorSchema.nullish(), avatarColor: UserAvatarColorSchema.nullish(),
@@ -96,7 +97,10 @@ export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {}
const UserAdminUpdateSchema = z const UserAdminUpdateSchema = z
.object({ .object({
email: toEmail.optional().describe('User email'), email: toEmail
.transform((val) => val.toLowerCase())
.optional()
.describe('User email'),
password: z.string().optional().describe('User password'), password: z.string().optional().describe('User password'),
pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable())
.optional() .optional()
@@ -135,7 +139,7 @@ const UserAdminResponseSchema = UserResponseSchema.extend({
license: UserLicenseSchema.nullable(), license: UserLicenseSchema.nullable(),
}).meta({ id: 'UserAdminResponseDto' }); }).meta({ id: 'UserAdminResponseDto' });
export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema, { codec: true }) {}
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
const metadata = entity.metadata || []; const metadata = entity.metadata || [];
+2 -2
View File
@@ -46,7 +46,7 @@ const WorkflowFilterResponseSchema = z
workflowId: z.string().describe('Workflow ID'), workflowId: z.string().describe('Workflow ID'),
pluginFilterId: z.string().describe('Plugin filter ID'), pluginFilterId: z.string().describe('Plugin filter ID'),
filterConfig: FilterConfigSchema.nullable(), filterConfig: FilterConfigSchema.nullable(),
order: z.int().describe('Filter order'), order: z.number().describe('Filter order'),
}) })
.meta({ id: 'WorkflowFilterResponseDto' }); .meta({ id: 'WorkflowFilterResponseDto' });
@@ -56,7 +56,7 @@ const WorkflowActionResponseSchema = z
workflowId: z.string().describe('Workflow ID'), workflowId: z.string().describe('Workflow ID'),
pluginActionId: z.string().describe('Plugin action ID'), pluginActionId: z.string().describe('Plugin action ID'),
actionConfig: ActionConfigSchema.nullable(), actionConfig: ActionConfigSchema.nullable(),
order: z.int().describe('Action order'), order: z.number().describe('Action order'),
}) })
.meta({ id: 'WorkflowActionResponseDto' }); .meta({ id: 'WorkflowActionResponseDto' });
+1 -7
View File
@@ -22,7 +22,7 @@ export enum ImmichHeader {
SharedLinkKey = 'x-immich-share-key', SharedLinkKey = 'x-immich-share-key',
SharedLinkSlug = 'x-immich-share-slug', SharedLinkSlug = 'x-immich-share-slug',
Checksum = 'x-immich-checksum', Checksum = 'x-immich-checksum',
CorrelationId = 'X-Correlation-ID', Cid = 'x-immich-cid',
} }
export enum ImmichQuery { export enum ImmichQuery {
@@ -445,12 +445,6 @@ export enum VideoCodec {
export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' }); export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' });
export enum VideoSegmentCodec {
Av1 = 'av1',
Hevc = 'hevc',
H264 = 'h264',
}
export enum AudioCodec { export enum AudioCodec {
Mp3 = 'mp3', Mp3 = 'mp3',
Aac = 'aac', Aac = 'aac',
@@ -2,7 +2,6 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/co
import { Response } from 'express'; import { Response } from 'express';
import { ClsService } from 'nestjs-cls'; import { ClsService } from 'nestjs-cls';
import { ZodSerializationException, ZodValidationException } from 'nestjs-zod'; import { ZodSerializationException, ZodValidationException } from 'nestjs-zod';
import { ImmichHeader } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { logGlobalError } from 'src/utils/logger'; import { logGlobalError } from 'src/utils/logger';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
@@ -17,13 +16,18 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
} }
catch(error: Error, host: ArgumentsHost) { catch(error: Error, host: ArgumentsHost) {
this.handleError(host.switchToHttp().getResponse<Response>(), error); const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const { status, body } = this.fromError(error);
if (!response.headersSent) {
response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
}
} }
handleError(res: Response, error: Error) { handleError(res: Response, error: Error) {
const { status, body } = this.fromError(error); const { status, body } = this.fromError(error);
if (!res.headersSent) { if (!res.headersSent) {
res.header(ImmichHeader.CorrelationId, this.cls.getId()).status(status).json(body); res.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
} }
} }
@@ -32,24 +36,26 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
if (error instanceof HttpException) { if (error instanceof HttpException) {
const status = error.getStatus(); const status = error.getStatus();
const response = error.getResponse(); let body = error.getResponse();
const body: Record<string, unknown> =
typeof response === 'string' ? { message: response } : { ...(response as object) }; // unclear what circumstances would return a string
if (typeof body === 'string') {
body = { message: body };
}
// handle both request and response validation errors // handle both request and response validation errors
if (error instanceof ZodValidationException || error instanceof ZodSerializationException) { if (error instanceof ZodValidationException || error instanceof ZodSerializationException) {
const zodError = error.getZodError(); const zodError = error.getZodError();
if (zodError instanceof ZodError && zodError.issues.length > 0) { if (zodError instanceof ZodError && zodError.issues.length > 0) {
body['message'] = zodError.issues.map((issue) => body = {
issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message, message: zodError.issues.map((issue) =>
); issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message,
),
error: 'Bad Request',
};
} }
} }
// remove fields that duplicate the HTTP response line or will be reformatted in a later step
delete body['error'];
delete body['statusCode'];
delete body['errors'];
return { status, body }; return { status, body };
} }
@@ -1,46 +0,0 @@
-- NOTE: This file is auto generated by ./sql-generator
-- VideoStreamRepository.getSession
select
*
from
"video_stream_session"
where
"id" = $1
-- VideoStreamRepository.getVariant
select
*
from
"video_stream_variant"
where
"id" = $1
-- VideoStreamRepository.getSegment
select
*
from
"video_stream_segment"
where
"variantId" = $1
and "index" = $2
-- VideoStreamRepository.getExpiredSessions
select
"id"
from
"video_stream_session"
where
"expiresAt" <= $1
-- VideoStreamRepository.extendSession
update "video_stream_session"
set
"expiresAt" = $1
where
"id" = $2
-- VideoStreamRepository.deleteSession
delete from "video_stream_session"
where
"id" = $1
+4 -2
View File
@@ -301,9 +301,11 @@ const getEnv = (): EnvData => {
mount: true, mount: true,
generateId: true, generateId: true,
setup: (cls, req: Request, res: Response) => { setup: (cls, req: Request, res: Response) => {
const cid = req.header(ImmichHeader.CorrelationId) || cls.get(CLS_ID); const headerValues = req.headers[ImmichHeader.Cid];
const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
const cid = headerValue || cls.get(CLS_ID);
cls.set(CLS_ID, cid); cls.set(CLS_ID, cid);
res.header(ImmichHeader.CorrelationId, cid); res.header(ImmichHeader.Cid, cid);
}, },
}, },
}, },
-2
View File
@@ -46,7 +46,6 @@ import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository'; import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { VideoStreamRepository } from 'src/repositories/video-stream.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
import { WebsocketRepository } from 'src/repositories/websocket.repository'; import { WebsocketRepository } from 'src/repositories/websocket.repository';
import { WorkflowRepository } from 'src/repositories/workflow.repository'; import { WorkflowRepository } from 'src/repositories/workflow.repository';
@@ -101,7 +100,6 @@ export const repositories = [
UserRepository, UserRepository,
ViewRepository, ViewRepository,
VersionHistoryRepository, VersionHistoryRepository,
VideoStreamRepository,
WebsocketRepository, WebsocketRepository,
WorkflowRepository, WorkflowRepository,
]; ];
@@ -1,62 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Insertable, Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DummyValue, GenerateSql } from 'src/decorators';
import { DB } from 'src/schema';
import {
VideoStreamSegmentTable,
VideoStreamSessionTable,
VideoStreamVariantTable,
} from 'src/schema/tables/video-stream.table';
@Injectable()
export class VideoStreamRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
createSession(session: Insertable<VideoStreamSessionTable>) {
return this.db.insertInto('video_stream_session').values(session).returning(['id']).executeTakeFirstOrThrow();
}
createVariant(variant: Insertable<VideoStreamVariantTable>) {
return this.db.insertInto('video_stream_variant').values(variant).returning(['id']).executeTakeFirstOrThrow();
}
async createSegment(segment: Insertable<VideoStreamSegmentTable>) {
await this.db.insertInto('video_stream_segment').values(segment).execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
getSession(id: string) {
return this.db.selectFrom('video_stream_session').selectAll().where('id', '=', id).executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID] })
getVariant(id: string) {
return this.db.selectFrom('video_stream_variant').selectAll().where('id', '=', id).executeTakeFirst();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
getSegment(variantId: string, index: number) {
return this.db
.selectFrom('video_stream_segment')
.selectAll()
.where('variantId', '=', variantId)
.where('index', '=', index)
.executeTakeFirst();
}
@GenerateSql()
getExpiredSessions() {
return this.db.selectFrom('video_stream_session').select(['id']).where('expiresAt', '<=', new Date()).execute();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] })
async extendSession(id: string, expiresAt: Date) {
await this.db.updateTable('video_stream_session').set({ expiresAt }).where('id', '=', id).execute();
}
@GenerateSql({ params: [DummyValue.UUID] })
async deleteSession(id: string) {
await this.db.deleteFrom('video_stream_session').where('id', '=', id).execute();
}
}
+1 -13
View File
@@ -1,12 +1,5 @@
import { registerEnum } from '@immich/sql-tools'; import { registerEnum } from '@immich/sql-tools';
import { import { AlbumUserRole, AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
AlbumUserRole,
AssetStatus,
AssetVisibility,
ChecksumAlgorithm,
SourceType,
VideoSegmentCodec,
} from 'src/enum';
export const album_user_role_enum = registerEnum({ export const album_user_role_enum = registerEnum({
name: 'album_user_role_enum', name: 'album_user_role_enum',
@@ -32,8 +25,3 @@ export const asset_checksum_algorithm_enum = registerEnum({
name: 'asset_checksum_algorithm_enum', name: 'asset_checksum_algorithm_enum',
values: Object.values(ChecksumAlgorithm), values: Object.values(ChecksumAlgorithm),
}); });
export const video_stream_variant_codec_enum = registerEnum({
name: 'video_stream_variant_codec_enum',
values: Object.values(VideoSegmentCodec),
});
-12
View File
@@ -76,11 +76,6 @@ import { UserMetadataAuditTable } from 'src/schema/tables/user-metadata-audit.ta
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
import { UserTable } from 'src/schema/tables/user.table'; import { UserTable } from 'src/schema/tables/user.table';
import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
import {
VideoStreamSegmentTable,
VideoStreamSessionTable,
VideoStreamVariantTable,
} from 'src/schema/tables/video-stream.table';
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table'; import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@@ -138,9 +133,6 @@ export class ImmichDatabase {
UserMetadataAuditTable, UserMetadataAuditTable,
UserTable, UserTable,
VersionHistoryTable, VersionHistoryTable,
VideoStreamSessionTable,
VideoStreamVariantTable,
VideoStreamSegmentTable,
PluginTable, PluginTable,
PluginFilterTable, PluginFilterTable,
PluginActionTable, PluginActionTable,
@@ -255,10 +247,6 @@ export interface DB {
version_history: VersionHistoryTable; version_history: VersionHistoryTable;
video_stream_session: VideoStreamSessionTable;
video_stream_variant: VideoStreamVariantTable;
video_stream_segment: VideoStreamSegmentTable;
plugin: PluginTable; plugin: PluginTable;
plugin_filter: PluginFilterTable; plugin_filter: PluginFilterTable;
plugin_action: PluginActionTable; plugin_action: PluginActionTable;
@@ -1,40 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE TYPE "video_stream_variant_codec_enum" AS ENUM ('av1','hevc','h264');`.execute(db);
await sql`CREATE TABLE "video_stream_session" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"assetId" uuid NOT NULL,
"expiresAt" timestamp with time zone NOT NULL,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "video_stream_session_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "video_stream_session_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "video_stream_session_assetId_idx" ON "video_stream_session" ("assetId");`.execute(db);
await sql`CREATE INDEX "video_stream_session_expiresAt_idx" ON "video_stream_session" ("expiresAt");`.execute(db);
await sql`CREATE TABLE "video_stream_variant" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"sessionId" uuid NOT NULL,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"bitrate" integer NOT NULL,
"codec" video_stream_variant_codec_enum NOT NULL,
"resolution" smallint NOT NULL,
CONSTRAINT "video_stream_variant_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "video_stream_session" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "video_stream_variant_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE UNIQUE INDEX "video_stream_variant_sessionId_bitrate_resolution_codec_idx" ON "video_stream_variant" ("sessionId", "bitrate", "resolution", "codec");`.execute(db);
await sql`CREATE TABLE "video_stream_segment" (
"variantId" uuid NOT NULL,
"index" integer NOT NULL,
"durationUs" integer NOT NULL,
CONSTRAINT "video_stream_segment_variantId_fkey" FOREIGN KEY ("variantId") REFERENCES "video_stream_variant" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "video_stream_segment_pkey" PRIMARY KEY ("variantId", "index")
);`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TABLE "video_stream_segment";`.execute(db);
await sql`DROP TABLE "video_stream_variant";`.execute(db);
await sql`DROP TABLE "video_stream_session";`.execute(db);
await sql`DROP TYPE "asset_checksum_algorithm_enum";`.execute(db);
}
@@ -1,63 +0,0 @@
import {
Column,
CreateDateColumn,
ForeignKeyColumn,
Generated,
Index,
PrimaryColumn,
PrimaryGeneratedColumn,
Table,
Timestamp,
} from '@immich/sql-tools';
import { VideoSegmentCodec } from 'src/enum';
import { video_stream_variant_codec_enum } from 'src/schema/enums';
import { AssetTable } from 'src/schema/tables/asset.table';
@Table('video_stream_session')
export class VideoStreamSessionTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE' })
assetId!: string;
@Column({ type: 'timestamp with time zone', index: true })
expiresAt!: Timestamp;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
}
@Index({ columns: ['sessionId', 'bitrate', 'resolution', 'codec'], unique: true })
@Table('video_stream_variant')
export class VideoStreamVariantTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@ForeignKeyColumn(() => VideoStreamSessionTable, { onDelete: 'CASCADE', index: false })
sessionId!: string;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@Column({ type: 'integer' })
bitrate!: number;
@Column({ enum: video_stream_variant_codec_enum })
codec!: VideoSegmentCodec;
@Column({ type: 'smallint' })
resolution!: number;
}
@Table('video_stream_segment')
export class VideoStreamSegmentTable {
@ForeignKeyColumn(() => VideoStreamVariantTable, { onDelete: 'CASCADE', primary: true, index: false })
variantId!: string;
@PrimaryColumn({ type: 'integer' })
index!: number;
@Column({ type: 'integer' })
durationUs!: number;
}
@@ -196,7 +196,6 @@ describe(AlbumService.name, () => {
expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {}); expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {});
expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id); expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id);
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([assetId]), false);
expect(mocks.event.emit).toHaveBeenCalledTimes(1);
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
id: album.id, id: album.id,
userId: albumUser.userId, userId: albumUser.userId,
+8 -8
View File
@@ -19,7 +19,6 @@ import { AlbumUserRole, Permission } from 'src/enum';
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
import { asDateString } from 'src/utils/date';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
@@ -63,11 +62,11 @@ export class AlbumService extends BaseService {
return albums.map((album) => ({ return albums.map((album) => ({
...mapAlbum(album), ...mapAlbum(album),
sharedLinks: undefined, sharedLinks: undefined,
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined), startDate: albumMetadata[album.id]?.startDate ?? undefined,
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined), endDate: albumMetadata[album.id]?.endDate ?? undefined,
assetCount: albumMetadata[album.id]?.assetCount ?? 0, assetCount: albumMetadata[album.id]?.assetCount ?? 0,
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
lastModifiedAssetTimestamp: asDateString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined), lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
})); }));
} }
@@ -83,10 +82,10 @@ export class AlbumService extends BaseService {
return { return {
...mapAlbum(album), ...mapAlbum(album),
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined), startDate: albumMetadataForIds?.startDate ?? undefined,
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined), endDate: albumMetadataForIds?.endDate ?? undefined,
assetCount: albumMetadataForIds?.assetCount ?? 0, assetCount: albumMetadataForIds?.assetCount ?? 0,
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined), lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined, contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
}; };
} }
@@ -114,6 +113,7 @@ export class AlbumService extends BaseService {
throw new BadRequestException('Cannot share album with owner'); throw new BadRequestException('Cannot share album with owner');
} }
} }
albumUsers.unshift({ userId: auth.user.id, role: AlbumUserRole.Owner });
const allowedAssetIdsSet = await this.checkAccess({ const allowedAssetIdsSet = await this.checkAccess({
auth, auth,
@@ -132,7 +132,7 @@ export class AlbumService extends BaseService {
order: getPreferences(userMetadata).albums.defaultAssetOrder, order: getPreferences(userMetadata).albums.defaultAssetOrder,
}, },
assetIds, assetIds,
[{ userId: auth.user.id, role: AlbumUserRole.Owner }, ...albumUsers], albumUsers,
auth.user.id, auth.user.id,
); );
-3
View File
@@ -53,7 +53,6 @@ import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository'; import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { VideoStreamRepository } from 'src/repositories/video-stream.repository';
import { ViewRepository } from 'src/repositories/view-repository'; import { ViewRepository } from 'src/repositories/view-repository';
import { WebsocketRepository } from 'src/repositories/websocket.repository'; import { WebsocketRepository } from 'src/repositories/websocket.repository';
import { WorkflowRepository } from 'src/repositories/workflow.repository'; import { WorkflowRepository } from 'src/repositories/workflow.repository';
@@ -110,7 +109,6 @@ export const BASE_SERVICE_DEPENDENCIES = [
TrashRepository, TrashRepository,
UserRepository, UserRepository,
VersionHistoryRepository, VersionHistoryRepository,
VideoStreamRepository,
ViewRepository, ViewRepository,
WebsocketRepository, WebsocketRepository,
WorkflowRepository, WorkflowRepository,
@@ -169,7 +167,6 @@ export class BaseService {
protected trashRepository: TrashRepository, protected trashRepository: TrashRepository,
protected userRepository: UserRepository, protected userRepository: UserRepository,
protected versionRepository: VersionHistoryRepository, protected versionRepository: VersionHistoryRepository,
protected videoStreamRepository: VideoStreamRepository,
protected viewRepository: ViewRepository, protected viewRepository: ViewRepository,
protected websocketRepository: WebsocketRepository, protected websocketRepository: WebsocketRepository,
protected workflowRepository: WorkflowRepository, protected workflowRepository: WorkflowRepository,
+6 -4
View File
@@ -211,11 +211,12 @@ describe(PersonService.name, () => {
await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({ await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({
id: person.id, id: person.id,
name: person.name, name: person.name,
birthDate: '1976-06-30', birthDate: new Date('1976-06-30'),
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: false, isHidden: false,
isFavorite: false, isFavorite: false,
updatedAt: expect.any(String), color: undefined,
updatedAt: expect.any(Date),
}); });
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' }); expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' });
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
@@ -485,10 +486,11 @@ describe(PersonService.name, () => {
birthDate: person.birthDate, birthDate: person.birthDate,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: undefined,
id: person.id, id: person.id,
name: person.name, name: person.name,
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
updatedAt: expect.any(String), updatedAt: expect.any(Date),
}); });
expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queue).not.toHaveBeenCalledWith();
@@ -848,7 +850,7 @@ describe(PersonService.name, () => {
facesRecognizedAt: expect.any(Date), facesRecognizedAt: expect.any(Date),
}); });
const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date; const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date;
expect(facesRecognizedAt.getTime()).toBeGreaterThan(start); expect(facesRecognizedAt.getTime()).toBeGreaterThanOrEqual(start);
}); });
it('should create a face with no person and queue recognition job', async () => { it('should create a face with no person and queue recognition job', async () => {

Some files were not shown because too many files have changed in this diff Show More