mirror of
https://github.com/immich-app/immich.git
synced 2026-06-03 20:55:25 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8c01fe953 | |||
| 4a8c3b60be | |||
| 2190aa72a8 | |||
| d21cb28526 | |||
| 5c33eb3204 | |||
| 53e4ae7fb2 | |||
| 47bf83813d | |||
| bd0fc2cc86 | |||
| e98201232a | |||
| 0388c534ed | |||
| 137687bc0f | |||
| 4ac4781bfb |
@@ -259,17 +259,6 @@ describe('/search', () => {
|
||||
assets: [assetHeic],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search city ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
city: '',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search city (null)',
|
||||
deferred: () => ({
|
||||
@@ -291,18 +280,6 @@ describe('/search', () => {
|
||||
assets: [assetDensity],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search state ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
state: '',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
withExif: true,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast, assetNotocactus],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search state (null)',
|
||||
deferred: () => ({
|
||||
@@ -324,17 +301,6 @@ describe('/search', () => {
|
||||
assets: [assetFalcon],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: "should search country ('')",
|
||||
deferred: () => ({
|
||||
dto: {
|
||||
country: '',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
includeNull: true,
|
||||
},
|
||||
assets: [assetLast],
|
||||
}),
|
||||
},
|
||||
{
|
||||
should: 'should search country (null)',
|
||||
deferred: () => ({
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:openapi/api.dart' show Optional;
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -137,7 +138,7 @@ class RemoteAlbumService {
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? description,
|
||||
Optional<String?> description = const Optional.absent(),
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/shared_link.provider.dart';
|
||||
import 'package:immich_mobile/services/shared_link.service.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -365,11 +366,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
bool? download;
|
||||
bool? upload;
|
||||
bool? meta;
|
||||
String? desc;
|
||||
String? password;
|
||||
var password = const Optional<String?>.absent();
|
||||
var description = const Optional<String?>.absent();
|
||||
String? slug;
|
||||
DateTime? expiry;
|
||||
bool? changeExpiry;
|
||||
var expiry = const Optional<DateTime?>.absent();
|
||||
|
||||
if (allowDownload.value != existingLink!.allowDownload) {
|
||||
download = allowDownload.value;
|
||||
@@ -383,12 +383,16 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
meta = showMetadata.value;
|
||||
}
|
||||
|
||||
if (descriptionController.text != existingLink!.description) {
|
||||
desc = descriptionController.text;
|
||||
if (descriptionController.text != (existingLink!.description ?? '')) {
|
||||
description = descriptionController.text.isEmpty
|
||||
? const Optional.present(null)
|
||||
: Optional.present(descriptionController.text);
|
||||
}
|
||||
|
||||
if (passwordController.text != existingLink!.password) {
|
||||
password = passwordController.text;
|
||||
if (passwordController.text != (existingLink!.password ?? '')) {
|
||||
password = passwordController.text.isEmpty
|
||||
? const Optional.present(null)
|
||||
: Optional.present(passwordController.text);
|
||||
}
|
||||
|
||||
if (slugController.text != (existingLink!.slug ?? "")) {
|
||||
@@ -399,8 +403,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
|
||||
final newExpiry = expiryAfter.value;
|
||||
if (newExpiry?.toUtc() != existingLink!.expiresAt?.toUtc()) {
|
||||
expiry = newExpiry;
|
||||
changeExpiry = true;
|
||||
expiry = newExpiry == null ? const Optional.present(null) : Optional.present(newExpiry.toUtc());
|
||||
}
|
||||
|
||||
await ref
|
||||
@@ -410,11 +413,10 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
showMeta: meta,
|
||||
allowDownload: download,
|
||||
allowUpload: upload,
|
||||
description: desc,
|
||||
description: description,
|
||||
password: password,
|
||||
slug: slug,
|
||||
expiresAt: expiry?.toUtc(),
|
||||
changeExpiry: changeExpiry,
|
||||
expiresAt: expiry,
|
||||
);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/remote_album_sliver_app_bar.dart';
|
||||
import 'package:openapi/api.dart' show Optional;
|
||||
|
||||
@RoutePage()
|
||||
class RemoteAlbumPage extends ConsumerStatefulWidget {
|
||||
@@ -247,10 +248,13 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> {
|
||||
try {
|
||||
final newTitle = titleController.text.trim();
|
||||
final newDescription = descriptionController.text.trim();
|
||||
final description = newDescription.isEmpty
|
||||
? const Optional<String?>.present(null)
|
||||
: Optional<String?>.present(newDescription);
|
||||
|
||||
await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.updateAlbum(widget.album.id, name: newTitle, description: newDescription);
|
||||
.updateAlbum(widget.album.id, name: newTitle, description: description);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:openapi/api.dart' show Optional;
|
||||
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -153,7 +154,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
Future<RemoteAlbum?> updateAlbum(
|
||||
String albumId, {
|
||||
String? name,
|
||||
String? description,
|
||||
Optional<String?> description = const Optional.absent(),
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
|
||||
@@ -71,7 +71,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
String albumId,
|
||||
UserDto owner, {
|
||||
String? name,
|
||||
String? description,
|
||||
Optional<String?> description = const Optional.absent(),
|
||||
String? thumbnailAssetId,
|
||||
bool? isActivityEnabled,
|
||||
AlbumAssetOrder? order,
|
||||
@@ -86,7 +86,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
albumId,
|
||||
UpdateAlbumDto(
|
||||
albumName: name == null ? const Optional.absent() : Optional.present(name),
|
||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
||||
description: description,
|
||||
albumThumbnailAssetId: thumbnailAssetId == null
|
||||
? const Optional.absent()
|
||||
: Optional.present(thumbnailAssetId),
|
||||
|
||||
@@ -88,11 +88,10 @@ class SharedLinkService {
|
||||
required bool? showMeta,
|
||||
required bool? allowDownload,
|
||||
required bool? allowUpload,
|
||||
bool? changeExpiry = false,
|
||||
String? description,
|
||||
String? password,
|
||||
Optional<String?> password = const Optional.absent(),
|
||||
Optional<String?> description = const Optional.absent(),
|
||||
String? slug,
|
||||
DateTime? expiresAt,
|
||||
Optional<DateTime?> expiresAt = const Optional.absent(),
|
||||
}) async {
|
||||
try {
|
||||
final responseDto = await _apiService.sharedLinksApi.updateSharedLink(
|
||||
@@ -101,11 +100,10 @@ class SharedLinkService {
|
||||
showMetadata: showMeta == null ? const Optional.absent() : Optional.present(showMeta),
|
||||
allowDownload: allowDownload == null ? const Optional.absent() : Optional.present(allowDownload),
|
||||
allowUpload: allowUpload == null ? const Optional.absent() : Optional.present(allowUpload),
|
||||
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
|
||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
||||
password: password == null ? const Optional.absent() : Optional.present(password),
|
||||
password: password,
|
||||
description: description,
|
||||
expiresAt: expiresAt,
|
||||
slug: slug == null ? const Optional.absent() : Optional.present(slug),
|
||||
changeExpiryTime: changeExpiry == null ? const Optional.absent() : Optional.present(changeExpiry),
|
||||
),
|
||||
);
|
||||
if (responseDto != null) {
|
||||
|
||||
+1
-18
@@ -15,7 +15,6 @@ class SharedLinkEditDto {
|
||||
SharedLinkEditDto({
|
||||
this.allowDownload = const Optional.absent(),
|
||||
this.allowUpload = const Optional.absent(),
|
||||
this.changeExpiryTime = const Optional.absent(),
|
||||
this.description = const Optional.absent(),
|
||||
this.expiresAt = const Optional.absent(),
|
||||
this.password = const Optional.absent(),
|
||||
@@ -41,15 +40,6 @@ class SharedLinkEditDto {
|
||||
///
|
||||
Optional<bool?> allowUpload;
|
||||
|
||||
/// Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.
|
||||
///
|
||||
/// 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
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
Optional<bool?> changeExpiryTime;
|
||||
|
||||
/// Link description
|
||||
Optional<String?> description;
|
||||
|
||||
@@ -75,7 +65,6 @@ class SharedLinkEditDto {
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkEditDto &&
|
||||
other.allowDownload == allowDownload &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.changeExpiryTime == changeExpiryTime &&
|
||||
other.description == description &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.password == password &&
|
||||
@@ -87,7 +76,6 @@ class SharedLinkEditDto {
|
||||
// ignore: unnecessary_parenthesis
|
||||
(allowDownload == null ? 0 : allowDownload!.hashCode) +
|
||||
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
||||
(changeExpiryTime == null ? 0 : changeExpiryTime!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(password == null ? 0 : password!.hashCode) +
|
||||
@@ -95,7 +83,7 @@ class SharedLinkEditDto {
|
||||
(slug == null ? 0 : slug!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, changeExpiryTime=$changeExpiryTime, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, slug=$slug]';
|
||||
String toString() => 'SharedLinkEditDto[allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, expiresAt=$expiresAt, password=$password, showMetadata=$showMetadata, slug=$slug]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -107,10 +95,6 @@ class SharedLinkEditDto {
|
||||
final value = this.allowUpload.value;
|
||||
json[r'allowUpload'] = value;
|
||||
}
|
||||
if (this.changeExpiryTime.isPresent) {
|
||||
final value = this.changeExpiryTime.value;
|
||||
json[r'changeExpiryTime'] = value;
|
||||
}
|
||||
if (this.description.isPresent) {
|
||||
final value = this.description.value;
|
||||
json[r'description'] = value;
|
||||
@@ -147,7 +131,6 @@ class SharedLinkEditDto {
|
||||
return SharedLinkEditDto(
|
||||
allowDownload: json.containsKey(r'allowDownload') ? Optional.present(mapValueOfType<bool>(json, r'allowDownload')) : const Optional.absent(),
|
||||
allowUpload: json.containsKey(r'allowUpload') ? Optional.present(mapValueOfType<bool>(json, r'allowUpload')) : const Optional.absent(),
|
||||
changeExpiryTime: json.containsKey(r'changeExpiryTime') ? Optional.present(mapValueOfType<bool>(json, r'changeExpiryTime')) : const Optional.absent(),
|
||||
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
|
||||
expiresAt: json.containsKey(r'expiresAt') ? Optional.present(mapDateTime(json, r'expiresAt', 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))$/')) : const Optional.absent(),
|
||||
password: json.containsKey(r'password') ? Optional.present(mapValueOfType<String>(json, r'password')) : const Optional.absent(),
|
||||
|
||||
@@ -22197,10 +22197,6 @@
|
||||
"description": "Allow uploads",
|
||||
"type": "boolean"
|
||||
},
|
||||
"changeExpiryTime": {
|
||||
"description": "Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"description": "Link description",
|
||||
"nullable": true,
|
||||
|
||||
@@ -2192,8 +2192,6 @@ export type SharedLinkEditDto = {
|
||||
allowDownload?: boolean;
|
||||
/** Allow uploads */
|
||||
allowUpload?: boolean;
|
||||
/** Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. */
|
||||
changeExpiryTime?: boolean;
|
||||
/** Link description */
|
||||
description?: string | null;
|
||||
/** Expiration date */
|
||||
|
||||
@@ -53,16 +53,6 @@ describe(PersonController.name, () => {
|
||||
await request(ctx.getHttpServer()).post('/people');
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should map an empty birthDate to null', async () => {
|
||||
await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' });
|
||||
expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null });
|
||||
});
|
||||
|
||||
it('should map an empty color to null', async () => {
|
||||
await request(ctx.getHttpServer()).post('/people').send({ color: '' });
|
||||
expect(service.create).toHaveBeenCalledWith(undefined, { color: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /people', () => {
|
||||
@@ -153,12 +143,6 @@ describe(PersonController.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should map an empty birthDate to null', async () => {
|
||||
const id = factory.uuid();
|
||||
await request(ctx.getHttpServer()).put(`/people/${id}`).send({ birthDate: '' });
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, id, { birthDate: null });
|
||||
});
|
||||
|
||||
it('should not accept an invalid birth date (false)', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/people/${factory.uuid()}`)
|
||||
|
||||
@@ -63,11 +63,5 @@ describe(TagController.name, () => {
|
||||
await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow setting a null color via an empty string', async () => {
|
||||
const id = factory.uuid();
|
||||
await request(ctx.getHttpServer()).put(`/tags/${id}`).send({ color: '' });
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ color: null }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,20 +9,22 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||
import { asBirthDateString, asDateString } from 'src/utils/date';
|
||||
import { transformFaceBoundingBox } from 'src/utils/transform';
|
||||
import { emptyStringToNull, hexColor, stringToBool } from 'src/validation';
|
||||
import { hexColor, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const PersonCreateSchema = z
|
||||
.object({
|
||||
name: z.string().optional().describe('Person name'),
|
||||
// Note: the mobile app cannot currently set the birth date to null.
|
||||
birthDate: emptyStringToNull(z.string().meta({ format: 'date' }).nullable())
|
||||
birthDate: z
|
||||
.string()
|
||||
.meta({ format: 'date' })
|
||||
.nullable()
|
||||
.optional()
|
||||
.refine((val) => (val ? new Date(val) <= new Date() : true), { error: 'Birth date cannot be in the future' })
|
||||
.describe('Person date of birth'),
|
||||
isHidden: z.boolean().optional().describe('Person visibility (hidden)'),
|
||||
isFavorite: z.boolean().optional().describe('Mark as favorite'),
|
||||
color: emptyStringToNull(hexColor.nullable()).optional().describe('Person color (hex)'),
|
||||
color: hexColor.nullable().optional().describe('Person color (hex)'),
|
||||
})
|
||||
.meta({ id: 'PersonCreateDto' });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HistoryBuilder } from 'src/decorators';
|
||||
import { AlbumResponseSchema } from 'src/dtos/album.dto';
|
||||
import { AssetResponseSchema } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder, AssetOrderSchema, AssetTypeSchema, AssetVisibilitySchema } from 'src/enum';
|
||||
import { emptyStringToNull, isoDatetimeToDate, stringToBool } from 'src/validation';
|
||||
import { isoDatetimeToDate, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const BaseSearchSchema = z.object({
|
||||
@@ -23,12 +23,12 @@ const BaseSearchSchema = z.object({
|
||||
trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)'),
|
||||
takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)'),
|
||||
takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)'),
|
||||
city: emptyStringToNull(z.string().nullable()).optional().describe('Filter by city name'),
|
||||
state: emptyStringToNull(z.string().nullable()).optional().describe('Filter by state/province name'),
|
||||
country: emptyStringToNull(z.string().nullable()).optional().describe('Filter by country name'),
|
||||
make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'),
|
||||
model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'),
|
||||
lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'),
|
||||
city: z.string().nullable().optional().describe('Filter by city name'),
|
||||
state: z.string().nullable().optional().describe('Filter by state/province name'),
|
||||
country: z.string().nullable().optional().describe('Filter by country name'),
|
||||
make: z.string().nullable().optional().describe('Filter by camera make'),
|
||||
model: z.string().nullable().optional().describe('Filter by camera model'),
|
||||
lensModel: z.string().nullable().optional().describe('Filter by lens model'),
|
||||
isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'),
|
||||
personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'),
|
||||
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HistoryBuilder } from 'src/decorators';
|
||||
import { AlbumResponseSchema, mapAlbum } from 'src/dtos/album.dto';
|
||||
import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkTypeSchema } from 'src/enum';
|
||||
import { emptyStringToNull, isoDatetimeToDate } from 'src/validation';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const SharedLinkSearchSchema = z
|
||||
@@ -23,9 +23,9 @@ const SharedLinkCreateSchema = z
|
||||
type: SharedLinkTypeSchema,
|
||||
assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs (for individual assets)'),
|
||||
albumId: z.uuidv4().optional().describe('Album ID (for album sharing)'),
|
||||
description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'),
|
||||
password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'),
|
||||
slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'),
|
||||
description: z.string().nullable().optional().describe('Link description'),
|
||||
password: z.string().nullable().optional().describe('Link password'),
|
||||
slug: z.string().nullable().optional().describe('Custom URL slug'),
|
||||
expiresAt: isoDatetimeToDate.nullable().describe('Expiration date').default(null).optional(),
|
||||
allowUpload: z.boolean().optional().describe('Allow uploads'),
|
||||
allowDownload: z.boolean().default(true).optional().describe('Allow downloads'),
|
||||
@@ -35,19 +35,13 @@ const SharedLinkCreateSchema = z
|
||||
|
||||
const SharedLinkEditSchema = z
|
||||
.object({
|
||||
description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'),
|
||||
password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'),
|
||||
slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'),
|
||||
description: z.string().nullable().optional().describe('Link description'),
|
||||
password: z.string().nullable().optional().describe('Link password'),
|
||||
slug: z.string().nullable().optional().describe('Custom URL slug'),
|
||||
expiresAt: isoDatetimeToDate.nullish().describe('Expiration date'),
|
||||
allowUpload: z.boolean().optional().describe('Allow uploads'),
|
||||
allowDownload: z.boolean().optional().describe('Allow downloads'),
|
||||
showMetadata: z.boolean().optional().describe('Show metadata'),
|
||||
changeExpiryTime: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.',
|
||||
),
|
||||
})
|
||||
.meta({ id: 'SharedLinkEditDto' });
|
||||
|
||||
|
||||
@@ -2,20 +2,20 @@ import { createZodDto } from 'nestjs-zod';
|
||||
import { Tag } from 'src/database';
|
||||
import { MaybeDehydrated } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { emptyStringToNull, hexColor } from 'src/validation';
|
||||
import { hexColor } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const TagCreateSchema = z
|
||||
.object({
|
||||
name: z.string().describe('Tag name'),
|
||||
parentId: z.uuidv4().nullish().describe('Parent tag ID'),
|
||||
color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'),
|
||||
color: hexColor.nullable().optional().describe('Tag color (hex)'),
|
||||
})
|
||||
.meta({ id: 'TagCreateDto' });
|
||||
|
||||
const TagUpdateSchema = z
|
||||
.object({
|
||||
color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'),
|
||||
color: hexColor.nullable().optional().describe('Tag color (hex)'),
|
||||
})
|
||||
.meta({ id: 'TagUpdateDto' });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { pinCodeRegex } from 'src/dtos/auth.dto';
|
||||
import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum';
|
||||
import { MaybeDehydrated, UserMetadataItem } from 'src/types';
|
||||
import { asDateString } from 'src/utils/date';
|
||||
import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
|
||||
import { isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export const UserUpdateMeSchema = z
|
||||
@@ -80,10 +80,7 @@ export const UserAdminCreateSchema = z
|
||||
password: z.string().describe('User password'),
|
||||
name: z.string().describe('User name'),
|
||||
avatarColor: UserAvatarColorSchema.nullish(),
|
||||
pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable())
|
||||
.optional()
|
||||
.describe('PIN code')
|
||||
.meta({ example: '123456' }),
|
||||
pinCode: z.string().regex(pinCodeRegex).nullable().optional().describe('PIN code').meta({ example: '123456' }),
|
||||
storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'),
|
||||
quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'),
|
||||
shouldChangePassword: z.boolean().optional().describe('Require password change on next login'),
|
||||
@@ -98,10 +95,7 @@ const UserAdminUpdateSchema = z
|
||||
.object({
|
||||
email: toEmail.optional().describe('User email'),
|
||||
password: z.string().optional().describe('User password'),
|
||||
pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable())
|
||||
.optional()
|
||||
.describe('PIN code')
|
||||
.meta({ example: '123456' }),
|
||||
pinCode: z.string().regex(pinCodeRegex).nullable().optional().describe('PIN code').meta({ example: '123456' }),
|
||||
name: z.string().optional().describe('User name'),
|
||||
avatarColor: UserAvatarColorSchema.nullish(),
|
||||
storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'),
|
||||
|
||||
@@ -24,7 +24,7 @@ import { DB } from 'src/schema';
|
||||
import { immich_uuid_v7 } from 'src/schema/functions';
|
||||
import { ExtensionVersion, VectorExtension } from 'src/types';
|
||||
import { vectorIndexQuery } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export let cachedVectorExtension: VectorExtension | undefined;
|
||||
export async function getVectorExtension(runner: Kysely<DB>): Promise<VectorExtension> {
|
||||
@@ -292,7 +292,13 @@ export class DatabaseRepository {
|
||||
`.execute(this.db);
|
||||
|
||||
const dimSize = rows[0]?.dimsize;
|
||||
if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) {
|
||||
if (
|
||||
!z
|
||||
.int()
|
||||
.min(1)
|
||||
.max(2 ** 16)
|
||||
.safeParse(dimSize).success
|
||||
) {
|
||||
this.logger.warn(`Could not retrieve dimension size of column '${column}' in table '${table}', assuming 512`);
|
||||
return 512;
|
||||
}
|
||||
@@ -300,7 +306,13 @@ export class DatabaseRepository {
|
||||
}
|
||||
|
||||
async setDimensionSize(dimSize: number): Promise<void> {
|
||||
if (!isValidInteger(dimSize, { min: 1, max: 2 ** 16 })) {
|
||||
if (
|
||||
!z
|
||||
.int()
|
||||
.min(1)
|
||||
.max(2 ** 16)
|
||||
.safeParse(dimSize).success
|
||||
) {
|
||||
throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { anyUuid, searchAssetBuilder, withExifInner } from 'src/utils/database';
|
||||
import { paginationHelper } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export interface SearchAssetIdOptions {
|
||||
checksum?: Buffer;
|
||||
@@ -278,7 +278,7 @@ export class SearchRepository {
|
||||
],
|
||||
})
|
||||
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions) {
|
||||
if (!isValidInteger(pagination.size, { min: 1, max: 1000 })) {
|
||||
if (!z.int().min(1).max(1000).safeParse(pagination.size).success) {
|
||||
throw new Error(`Invalid value for 'size': ${pagination.size}`);
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ export class SearchRepository {
|
||||
],
|
||||
})
|
||||
searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) {
|
||||
if (!isValidInteger(numResults, { min: 1, max: 1000 })) {
|
||||
if (!z.int().min(1).max(1000).safeParse(numResults).success) {
|
||||
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ export class SharedLinkService extends BaseService {
|
||||
userId: auth.user.id,
|
||||
description: dto.description,
|
||||
password: dto.password,
|
||||
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
|
||||
expiresAt: dto.expiresAt,
|
||||
allowUpload: dto.allowUpload,
|
||||
allowDownload: dto.allowDownload,
|
||||
showExif: dto.showMetadata,
|
||||
|
||||
@@ -125,11 +125,6 @@ const FilenameParamSchema = z.object({
|
||||
|
||||
export class FilenameParamDto extends createZodDto(FilenameParamSchema) {}
|
||||
|
||||
export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => {
|
||||
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options;
|
||||
return Number.isInteger(value) && value >= min && value <= max;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified email validation
|
||||
* Converts email strings to lowercase and validates against HTML5 email regex
|
||||
@@ -251,16 +246,4 @@ export const hexColor = z
|
||||
.regex(hexColorRegex)
|
||||
.transform((val) => (val.startsWith('#') ? val : `#${val}`));
|
||||
|
||||
/**
|
||||
* Transform empty strings to null. Inner schema passed to this function must accept null.
|
||||
* @docs https://zod.dev/api?id=preprocess
|
||||
* @example emptyStringToNull(z.string().nullable()).optional() // [encouraged] final schema is optional
|
||||
* @example emptyStringToNull(z.string().nullable()) // [encouraged] same as the one above, but final schema is not optional
|
||||
* @example emptyStringToNull(z.string().nullish()) // [discouraged] same as the one above, might be confusing
|
||||
* @example emptyStringToNull(z.string().optional()) // fails: string schema rejects null
|
||||
* @example emptyStringToNull(z.string().nullable()).nullish() // [discouraged] passes, null is duplicated. use the first example instead
|
||||
*/
|
||||
export const emptyStringToNull = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
z.preprocess((val) => (val === '' ? null : val), schema);
|
||||
|
||||
export const sanitizeFilename = z.string().transform((val) => sanitize(val.replaceAll('.', '')));
|
||||
|
||||
@@ -370,6 +370,7 @@
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
slot="media"
|
||||
src={assetFileUrl}
|
||||
loop={$loopVideoPreference && loopVideo}
|
||||
autoplay={$autoPlayVideo}
|
||||
disablePictureInPicture
|
||||
|
||||
Reference in New Issue
Block a user