diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 341b88421c..891c64cc9a 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index 6a987df43c..9bc1957c4d 100644 Binary files a/mobile/openapi/doc/AlbumResponseDto.md and b/mobile/openapi/doc/AlbumResponseDto.md differ diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index aa7d5494cc..bcb0120cd1 100644 Binary files a/mobile/openapi/doc/AssetResponseDto.md and b/mobile/openapi/doc/AssetResponseDto.md differ diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 7858e857d6..b2264f0d3d 100644 Binary files a/mobile/openapi/lib/model/album_response_dto.dart and b/mobile/openapi/lib/model/album_response_dto.dart differ diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 2f516e2bda..3842f5ab90 100644 Binary files a/mobile/openapi/lib/model/asset_response_dto.dart and b/mobile/openapi/lib/model/asset_response_dto.dart differ diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index 1f16d754f9..a91dcf8772 100644 Binary files a/mobile/openapi/test/album_response_dto_test.dart and b/mobile/openapi/test/album_response_dto_test.dart differ diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index 523fc52814..f4cce051e2 100644 Binary files a/mobile/openapi/test/asset_response_dto_test.dart and b/mobile/openapi/test/asset_response_dto_test.dart differ diff --git a/server/apps/immich/src/api-v1/album/album-repository.ts b/server/apps/immich/src/api-v1/album/album-repository.ts index f9392fa166..07f05c03ac 100644 --- a/server/apps/immich/src/api-v1/album/album-repository.ts +++ b/server/apps/immich/src/api-v1/album/album-repository.ts @@ -213,18 +213,27 @@ export class AlbumRepository implements IAlbumRepository { } async get(albumId: string): Promise { - const query = this.albumRepository.createQueryBuilder('album'); - - const album = await query - .where('album.id = :albumId', { albumId }) - .leftJoinAndSelect('album.sharedUsers', 'sharedUser') - .leftJoinAndSelect('sharedUser.userInfo', 'userInfo') - .leftJoinAndSelect('album.assets', 'assets') - .leftJoinAndSelect('assets.assetInfo', 'assetInfo') - .leftJoinAndSelect('assetInfo.exifInfo', 'exifInfo') - .leftJoinAndSelect('album.sharedLinks', 'sharedLinks') - .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC') - .getOne(); + const album = await this.albumRepository.findOne({ + where: { id: albumId }, + relations: { + sharedUsers: { + userInfo: true, + }, + assets: { + assetInfo: { + exifInfo: true, + }, + }, + sharedLinks: true, + }, + order: { + assets: { + assetInfo: { + createdAt: 'ASC', + }, + }, + }, + }); if (!album) { return; @@ -249,11 +258,14 @@ export class AlbumRepository implements IAlbumRepository { } await this.userAlbumRepository.save([...newRecords]); + await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() }); + return this.get(album.id) as Promise; // There is an album for sure } async removeUser(album: AlbumEntity, userId: string): Promise { await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId }); + await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() }); } async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise { @@ -262,6 +274,8 @@ export class AlbumRepository implements IAlbumRepository { assetId: In(removeAssetsDto.assetIds), }); + await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() }); + return res.affected || 0; } @@ -290,6 +304,8 @@ export class AlbumRepository implements IAlbumRepository { await this.assetAlbumRepository.save([...newRecords]); + await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() }); + return { successfullyAdded: newRecords.length, alreadyInAlbum: alreadyExisting, diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index eb00f5ac34..6733db5605 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -32,6 +32,7 @@ describe('Album service', () => { albumEntity.id = albumId; albumEntity.albumName = 'name'; albumEntity.createdAt = 'date'; + albumEntity.updatedAt = 'date'; albumEntity.sharedUsers = []; albumEntity.assets = []; albumEntity.albumThumbnailAssetId = null; @@ -183,6 +184,7 @@ describe('Album service', () => { albumName: 'name', albumThumbnailAssetId: null, createdAt: 'date', + updatedAt: 'date', id: 'f19ab956-4761-41ea-a5d6-bae948308d58', ownerId, shared: false, diff --git a/server/apps/immich/src/api-v1/asset/asset.core.ts b/server/apps/immich/src/api-v1/asset/asset.core.ts index 5ad47c70ab..5fbe9c8816 100644 --- a/server/apps/immich/src/api-v1/asset/asset.core.ts +++ b/server/apps/immich/src/api-v1/asset/asset.core.ts @@ -27,6 +27,7 @@ export class AssetCore { createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(), modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(), + updatedAt: new Date().toISOString(), deviceAssetId: dto.deviceAssetId, deviceId: dto.deviceId, diff --git a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts index 7a906012af..395a375738 100644 --- a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts +++ b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts @@ -23,6 +23,7 @@ describe('TagService', () => { shouldChangePassword: true, createdAt: '2022-12-02T19:29:23.603Z', deletedAt: undefined, + updatedAt: '2022-12-02T19:29:23.603Z', tags: [], oauthId: 'oauth-id-1', }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index f786c4693a..5a7b3380b6 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3291,6 +3291,9 @@ "modifiedAt": { "type": "string" }, + "updatedAt": { + "type": "string" + }, "isFavorite": { "type": "boolean" }, @@ -3336,6 +3339,7 @@ "resizePath", "createdAt", "modifiedAt", + "updatedAt", "isFavorite", "mimeType", "duration", @@ -3361,6 +3365,9 @@ "createdAt": { "type": "string" }, + "updatedAt": { + "type": "string" + }, "albumThumbnailAssetId": { "type": "string", "nullable": true @@ -3387,6 +3394,7 @@ "ownerId", "albumName", "createdAt", + "updatedAt", "albumThumbnailAssetId", "shared", "sharedUsers", diff --git a/server/libs/domain/src/album/response-dto/album-response.dto.ts b/server/libs/domain/src/album/response-dto/album-response.dto.ts index 0be9173f46..651e18ed25 100644 --- a/server/libs/domain/src/album/response-dto/album-response.dto.ts +++ b/server/libs/domain/src/album/response-dto/album-response.dto.ts @@ -8,6 +8,7 @@ export class AlbumResponseDto { ownerId!: string; albumName!: string; createdAt!: string; + updatedAt!: string; albumThumbnailAssetId!: string | null; shared!: boolean; sharedUsers!: UserResponseDto[]; @@ -30,6 +31,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { albumName: entity.albumName, albumThumbnailAssetId: entity.albumThumbnailAssetId, createdAt: entity.createdAt, + updatedAt: entity.updatedAt, id: entity.id, ownerId: entity.ownerId, sharedUsers, @@ -52,6 +54,7 @@ export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto albumName: entity.albumName, albumThumbnailAssetId: entity.albumThumbnailAssetId, createdAt: entity.createdAt, + updatedAt: entity.updatedAt, id: entity.id, ownerId: entity.ownerId, sharedUsers, diff --git a/server/libs/domain/src/asset/response-dto/asset-response.dto.ts b/server/libs/domain/src/asset/response-dto/asset-response.dto.ts index dc09f6d8c6..d72967723c 100644 --- a/server/libs/domain/src/asset/response-dto/asset-response.dto.ts +++ b/server/libs/domain/src/asset/response-dto/asset-response.dto.ts @@ -16,6 +16,7 @@ export class AssetResponseDto { resizePath!: string | null; createdAt!: string; modifiedAt!: string; + updatedAt!: string; isFavorite!: boolean; mimeType!: string | null; duration!: string; @@ -38,6 +39,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { resizePath: entity.resizePath, createdAt: entity.createdAt, modifiedAt: entity.modifiedAt, + updatedAt: entity.updatedAt, isFavorite: entity.isFavorite, mimeType: entity.mimeType, webpPath: entity.webpPath, @@ -61,6 +63,7 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { resizePath: entity.resizePath, createdAt: entity.createdAt, modifiedAt: entity.modifiedAt, + updatedAt: entity.updatedAt, isFavorite: entity.isFavorite, mimeType: entity.mimeType, webpPath: entity.webpPath, diff --git a/server/libs/domain/src/user/user.service.spec.ts b/server/libs/domain/src/user/user.service.spec.ts index 9235c32580..938e324f5b 100644 --- a/server/libs/domain/src/user/user.service.spec.ts +++ b/server/libs/domain/src/user/user.service.spec.ts @@ -31,6 +31,7 @@ const adminUser: UserEntity = Object.freeze({ shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + updatedAt: '2021-01-01', tags: [], }); @@ -45,6 +46,7 @@ const immichUser: UserEntity = Object.freeze({ shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + updatedAt: '2021-01-01', tags: [], }); @@ -59,6 +61,7 @@ const updatedImmichUser: UserEntity = Object.freeze({ shouldChangePassword: true, profileImagePath: '', createdAt: '2021-01-01', + updatedAt: '2021-01-01', tags: [], }); diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index 3cc9a17f32..e25fab90ae 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -48,6 +48,7 @@ const assetResponse: AssetResponseDto = { resizePath: '', createdAt: today.toISOString(), modifiedAt: today.toISOString(), + updatedAt: today.toISOString(), isFavorite: false, mimeType: 'image/jpeg', smartInfo: { @@ -67,6 +68,7 @@ const albumResponse: AlbumResponseDto = { albumName: 'Test Album', albumThumbnailAssetId: null, createdAt: today.toISOString(), + updatedAt: today.toISOString(), id: 'album-123', ownerId: 'admin_id', sharedUsers: [], @@ -126,6 +128,7 @@ export const userEntityStub = { shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + updatedAt: '2021-01-01', tags: [], }), user1: Object.freeze({ @@ -137,6 +140,7 @@ export const userEntityStub = { shouldChangePassword: false, profileImagePath: '', createdAt: '2021-01-01', + updatedAt: '2021-01-01', tags: [], }), }; @@ -329,6 +333,7 @@ export const sharedLinkStub = { ownerId: authStub.admin.id, albumName: 'Test Album', createdAt: today.toISOString(), + updatedAt: today.toISOString(), albumThumbnailAssetId: null, sharedUsers: [], sharedLinks: [], @@ -348,6 +353,7 @@ export const sharedLinkStub = { resizePath: '', createdAt: today.toISOString(), modifiedAt: today.toISOString(), + updatedAt: today.toISOString(), isFavorite: false, mimeType: 'image/jpeg', smartInfo: { diff --git a/server/libs/infra/src/db/entities/album.entity.ts b/server/libs/infra/src/db/entities/album.entity.ts index 53c06a53fc..a62c0c5528 100644 --- a/server/libs/infra/src/db/entities/album.entity.ts +++ b/server/libs/infra/src/db/entities/album.entity.ts @@ -1,4 +1,4 @@ -import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { AssetAlbumEntity } from './asset-album.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { UserAlbumEntity } from './user-album.entity'; @@ -17,6 +17,9 @@ export class AlbumEntity { @CreateDateColumn({ type: 'timestamptz' }) createdAt!: string; + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: string; + @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true }) albumThumbnailAssetId!: string | null; diff --git a/server/libs/infra/src/db/entities/asset.entity.ts b/server/libs/infra/src/db/entities/asset.entity.ts index 532793e1c3..5f716da34e 100644 --- a/server/libs/infra/src/db/entities/asset.entity.ts +++ b/server/libs/infra/src/db/entities/asset.entity.ts @@ -1,4 +1,14 @@ -import { Column, Entity, Index, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + Column, + Entity, + Index, + JoinTable, + ManyToMany, + OneToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; import { ExifEntity } from './exif.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { SmartInfoEntity } from './smart-info.entity'; @@ -40,6 +50,9 @@ export class AssetEntity { @Column({ type: 'timestamptz' }) modifiedAt!: string; + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: string; + @Column({ type: 'boolean', default: false }) isFavorite!: boolean; diff --git a/server/libs/infra/src/db/entities/user.entity.ts b/server/libs/infra/src/db/entities/user.entity.ts index 325c36db81..d8724aab9a 100644 --- a/server/libs/infra/src/db/entities/user.entity.ts +++ b/server/libs/infra/src/db/entities/user.entity.ts @@ -1,4 +1,12 @@ -import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; import { TagEntity } from './tag.entity'; @Entity('users') @@ -36,6 +44,9 @@ export class UserEntity { @DeleteDateColumn() deletedAt?: Date; + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: string; + @OneToMany(() => TagEntity, (tag) => tag.user) tags!: TagEntity[]; } diff --git a/server/libs/infra/src/db/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts b/server/libs/infra/src/db/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts new file mode 100644 index 0000000000..90c00b38e9 --- /dev/null +++ b/server/libs/infra/src/db/migrations/1675667878312-AddUpdatedAtColumnToAlbumsUsersAssets.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUpdatedAtColumnToAlbumsUsersAssets1675667878312 implements MigrationInterface { + name = 'AddUpdatedAtColumnToAlbumsUsersAssets1675667878312'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "users" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "assets" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "updatedAt"`); + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 6d0ac70362..e741d516d6 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.1 + * The version of the OpenAPI document: 1.45.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -246,6 +246,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'createdAt': string; + /** + * + * @type {string} + * @memberof AlbumResponseDto + */ + 'updatedAt': string; /** * * @type {string} @@ -462,6 +468,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'modifiedAt': string; + /** + * + * @type {string} + * @memberof AssetResponseDto + */ + 'updatedAt': string; /** * * @type {boolean} diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index f022bd6e33..eb55f2cd71 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.1 + * The version of the OpenAPI document: 1.45.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index d41e01f325..974b67d37d 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.1 + * The version of the OpenAPI document: 1.45.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index b12628461b..7223695d8b 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.1 + * The version of the OpenAPI document: 1.45.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 052d483f15..382b9a0b88 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.1 + * The version of the OpenAPI document: 1.45.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).