From bf47147fbba25c34ba7609d90882a190cbaae6b4 Mon Sep 17 00:00:00 2001 From: Mees Frensel <33722705+meesfrensel@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:26:34 +0100 Subject: [PATCH] fix(server): accept showAt and hideAt for creating memories (#26429) * fix(server): accept showAt and hideAt for creating memories * fix history --- .../openapi/lib/model/memory_create_dto.dart | 38 ++++++++++++++++++- open-api/immich-openapi-specs.json | 32 ++++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 4 ++ .../src/controllers/memory.controller.spec.ts | 14 +++++++ server/src/dtos/memory.dto.ts | 15 ++++++++ server/src/services/memory.service.ts | 2 + server/src/validation.ts | 4 +- 7 files changed, 106 insertions(+), 3 deletions(-) diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 7fd938b31a..5b8eeed8fb 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -15,9 +15,11 @@ class MemoryCreateDto { MemoryCreateDto({ this.assetIds = const [], required this.data, + this.hideAt, this.isSaved, required this.memoryAt, this.seenAt, + this.showAt, required this.type, }); @@ -26,6 +28,15 @@ class MemoryCreateDto { OnThisDayDto data; + /// Date when memory should be hidden + /// + /// 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. + /// + DateTime? hideAt; + /// Is memory saved /// /// Please note: This property should have been non-nullable! Since the specification file @@ -47,6 +58,15 @@ class MemoryCreateDto { /// DateTime? seenAt; + /// Date when memory should be shown + /// + /// 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. + /// + DateTime? showAt; + /// Memory type MemoryType type; @@ -54,9 +74,11 @@ class MemoryCreateDto { bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && _deepEquality.equals(other.assetIds, assetIds) && other.data == data && + other.hideAt == hideAt && other.isSaved == isSaved && other.memoryAt == memoryAt && other.seenAt == seenAt && + other.showAt == showAt && other.type == type; @override @@ -64,18 +86,25 @@ class MemoryCreateDto { // ignore: unnecessary_parenthesis (assetIds.hashCode) + (data.hashCode) + + (hideAt == null ? 0 : hideAt!.hashCode) + (isSaved == null ? 0 : isSaved!.hashCode) + (memoryAt.hashCode) + (seenAt == null ? 0 : seenAt!.hashCode) + + (showAt == null ? 0 : showAt!.hashCode) + (type.hashCode); @override - String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, type=$type]'; + String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, hideAt=$hideAt, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, showAt=$showAt, type=$type]'; Map toJson() { final json = {}; json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; + if (this.hideAt != null) { + json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + } else { + // json[r'hideAt'] = null; + } if (this.isSaved != null) { json[r'isSaved'] = this.isSaved; } else { @@ -86,6 +115,11 @@ class MemoryCreateDto { json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; + } + if (this.showAt != null) { + json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + } else { + // json[r'showAt'] = null; } json[r'type'] = this.type; return json; @@ -104,9 +138,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, + hideAt: mapDateTime(json, r'hideAt', r''), isSaved: mapValueOfType(json, r'isSaved'), memoryAt: mapDateTime(json, r'memoryAt', r'')!, seenAt: mapDateTime(json, r'seenAt', r''), + showAt: mapDateTime(json, r'showAt', r''), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 264781db59..f7702b0ce4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18651,6 +18651,22 @@ "data": { "$ref": "#/components/schemas/OnThisDayDto" }, + "hideAt": { + "description": "Date when memory should be hidden", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "isSaved": { "description": "Is memory saved", "type": "boolean" @@ -18665,6 +18681,22 @@ "format": "date-time", "type": "string" }, + "showAt": { + "description": "Date when memory should be shown", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "type": { "allOf": [ { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8f65e59cfa..7c1f940a91 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1404,12 +1404,16 @@ export type MemoryCreateDto = { /** Asset IDs to associate with memory */ assetIds?: string[]; data: OnThisDayDto; + /** Date when memory should be hidden */ + hideAt?: string; /** Is memory saved */ isSaved?: boolean; /** Memory date */ memoryAt: string; /** Date when memory was seen */ seenAt?: string; + /** Date when memory should be shown */ + showAt?: string; /** Memory type */ "type": MemoryType; }; diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 8629b6c799..820819ee6e 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -51,6 +51,20 @@ describe(MemoryController.name, () => { errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), ); }); + + it('should accept showAt and hideAt', async () => { + const { status } = await request(ctx.getHttpServer()) + .post('/memories') + .send({ + type: 'on_this_day', + data: { year: 2020 }, + memoryAt: new Date(2021).toISOString(), + showAt: new Date(2022).toISOString(), + hideAt: new Date(2023).toISOString(), + }); + + expect(status).toBe(201); + }); }); describe('GET /memories/statistics', () => { diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 0d73c19b20..edf65ef583 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; +import { HistoryBuilder } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetOrderWithRandom, MemoryType } from 'src/enum'; @@ -77,6 +78,20 @@ export class MemoryCreateDto extends MemoryBaseDto { @ValidateDate({ description: 'Memory date' }) memoryAt!: Date; + @ValidateDate({ + optional: true, + description: 'Date when memory should be shown', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + showAt?: Date; + + @ValidateDate({ + optional: true, + description: 'Date when memory should be hidden', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + hideAt?: Date; + @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) assetIds?: string[]; } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index db682b6393..2378d594e1 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -100,6 +100,8 @@ export class MemoryService extends BaseService { data: dto.data, isSaved: dto.isSaved, memoryAt: dto.memoryAt, + showAt: dto.showAt, + hideAt: dto.hideAt, seenAt: dto.seenAt, }, allowedAssetIds, diff --git a/server/src/validation.ts b/server/src/validation.ts index cdca1bc0ca..ce7ceb602f 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -233,7 +233,7 @@ export const ValidateHexColor = () => { }; type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { +export const ValidateDate = (options?: DateOptions & PropertyOptions) => { const { optional, nullable = false, @@ -243,7 +243,7 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { } = options || {}; return applyDecorators( - ApiProperty({ format, ...apiPropertyOptions }), + Property({ format, ...apiPropertyOptions }), IsDate(), optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), Transform(({ key, value }) => {