1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

feat(server): add updatedAt to Asset, Album and User (#1566)

* feat: add updatedAt info to DTO and generate api

* chore: remove unsued file

* chore: Add update statement to add/remove asset/user to album

* fix: test
This commit is contained in:
Alex 2023-02-06 10:24:58 -06:00 committed by GitHub
parent b8d2f5b373
commit 29bb1f7ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 119 additions and 20 deletions

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -213,18 +213,27 @@ export class AlbumRepository implements IAlbumRepository {
} }
async get(albumId: string): Promise<AlbumEntity | undefined> { async get(albumId: string): Promise<AlbumEntity | undefined> {
const query = this.albumRepository.createQueryBuilder('album'); const album = await this.albumRepository.findOne({
where: { id: albumId },
const album = await query relations: {
.where('album.id = :albumId', { albumId }) sharedUsers: {
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') userInfo: true,
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') },
.leftJoinAndSelect('album.assets', 'assets') assets: {
.leftJoinAndSelect('assets.assetInfo', 'assetInfo') assetInfo: {
.leftJoinAndSelect('assetInfo.exifInfo', 'exifInfo') exifInfo: true,
.leftJoinAndSelect('album.sharedLinks', 'sharedLinks') },
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC') },
.getOne(); sharedLinks: true,
},
order: {
assets: {
assetInfo: {
createdAt: 'ASC',
},
},
},
});
if (!album) { if (!album) {
return; return;
@ -249,11 +258,14 @@ export class AlbumRepository implements IAlbumRepository {
} }
await this.userAlbumRepository.save([...newRecords]); await this.userAlbumRepository.save([...newRecords]);
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure
} }
async removeUser(album: AlbumEntity, userId: string): Promise<void> { async removeUser(album: AlbumEntity, userId: string): Promise<void> {
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId }); 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<number> { async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
@ -262,6 +274,8 @@ export class AlbumRepository implements IAlbumRepository {
assetId: In(removeAssetsDto.assetIds), assetId: In(removeAssetsDto.assetIds),
}); });
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
return res.affected || 0; return res.affected || 0;
} }
@ -290,6 +304,8 @@ export class AlbumRepository implements IAlbumRepository {
await this.assetAlbumRepository.save([...newRecords]); await this.assetAlbumRepository.save([...newRecords]);
await this.albumRepository.update({ id: album.id }, { updatedAt: new Date().toISOString() });
return { return {
successfullyAdded: newRecords.length, successfullyAdded: newRecords.length,
alreadyInAlbum: alreadyExisting, alreadyInAlbum: alreadyExisting,

View File

@ -32,6 +32,7 @@ describe('Album service', () => {
albumEntity.id = albumId; albumEntity.id = albumId;
albumEntity.albumName = 'name'; albumEntity.albumName = 'name';
albumEntity.createdAt = 'date'; albumEntity.createdAt = 'date';
albumEntity.updatedAt = 'date';
albumEntity.sharedUsers = []; albumEntity.sharedUsers = [];
albumEntity.assets = []; albumEntity.assets = [];
albumEntity.albumThumbnailAssetId = null; albumEntity.albumThumbnailAssetId = null;
@ -183,6 +184,7 @@ describe('Album service', () => {
albumName: 'name', albumName: 'name',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: 'date', createdAt: 'date',
updatedAt: 'date',
id: 'f19ab956-4761-41ea-a5d6-bae948308d58', id: 'f19ab956-4761-41ea-a5d6-bae948308d58',
ownerId, ownerId,
shared: false, shared: false,

View File

@ -27,6 +27,7 @@ export class AssetCore {
createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(), createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(),
modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(), modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
deviceAssetId: dto.deviceAssetId, deviceAssetId: dto.deviceAssetId,
deviceId: dto.deviceId, deviceId: dto.deviceId,

View File

@ -23,6 +23,7 @@ describe('TagService', () => {
shouldChangePassword: true, shouldChangePassword: true,
createdAt: '2022-12-02T19:29:23.603Z', createdAt: '2022-12-02T19:29:23.603Z',
deletedAt: undefined, deletedAt: undefined,
updatedAt: '2022-12-02T19:29:23.603Z',
tags: [], tags: [],
oauthId: 'oauth-id-1', oauthId: 'oauth-id-1',
}); });

View File

@ -3291,6 +3291,9 @@
"modifiedAt": { "modifiedAt": {
"type": "string" "type": "string"
}, },
"updatedAt": {
"type": "string"
},
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
}, },
@ -3336,6 +3339,7 @@
"resizePath", "resizePath",
"createdAt", "createdAt",
"modifiedAt", "modifiedAt",
"updatedAt",
"isFavorite", "isFavorite",
"mimeType", "mimeType",
"duration", "duration",
@ -3361,6 +3365,9 @@
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
"updatedAt": {
"type": "string"
},
"albumThumbnailAssetId": { "albumThumbnailAssetId": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@ -3387,6 +3394,7 @@
"ownerId", "ownerId",
"albumName", "albumName",
"createdAt", "createdAt",
"updatedAt",
"albumThumbnailAssetId", "albumThumbnailAssetId",
"shared", "shared",
"sharedUsers", "sharedUsers",

View File

@ -8,6 +8,7 @@ export class AlbumResponseDto {
ownerId!: string; ownerId!: string;
albumName!: string; albumName!: string;
createdAt!: string; createdAt!: string;
updatedAt!: string;
albumThumbnailAssetId!: string | null; albumThumbnailAssetId!: string | null;
shared!: boolean; shared!: boolean;
sharedUsers!: UserResponseDto[]; sharedUsers!: UserResponseDto[];
@ -30,6 +31,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
albumName: entity.albumName, albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId, albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt, createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
id: entity.id, id: entity.id,
ownerId: entity.ownerId, ownerId: entity.ownerId,
sharedUsers, sharedUsers,
@ -52,6 +54,7 @@ export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto
albumName: entity.albumName, albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId, albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt, createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
id: entity.id, id: entity.id,
ownerId: entity.ownerId, ownerId: entity.ownerId,
sharedUsers, sharedUsers,

View File

@ -16,6 +16,7 @@ export class AssetResponseDto {
resizePath!: string | null; resizePath!: string | null;
createdAt!: string; createdAt!: string;
modifiedAt!: string; modifiedAt!: string;
updatedAt!: string;
isFavorite!: boolean; isFavorite!: boolean;
mimeType!: string | null; mimeType!: string | null;
duration!: string; duration!: string;
@ -38,6 +39,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
resizePath: entity.resizePath, resizePath: entity.resizePath,
createdAt: entity.createdAt, createdAt: entity.createdAt,
modifiedAt: entity.modifiedAt, modifiedAt: entity.modifiedAt,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite, isFavorite: entity.isFavorite,
mimeType: entity.mimeType, mimeType: entity.mimeType,
webpPath: entity.webpPath, webpPath: entity.webpPath,
@ -61,6 +63,7 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
resizePath: entity.resizePath, resizePath: entity.resizePath,
createdAt: entity.createdAt, createdAt: entity.createdAt,
modifiedAt: entity.modifiedAt, modifiedAt: entity.modifiedAt,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite, isFavorite: entity.isFavorite,
mimeType: entity.mimeType, mimeType: entity.mimeType,
webpPath: entity.webpPath, webpPath: entity.webpPath,

View File

@ -31,6 +31,7 @@ const adminUser: UserEntity = Object.freeze({
shouldChangePassword: false, shouldChangePassword: false,
profileImagePath: '', profileImagePath: '',
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01',
tags: [], tags: [],
}); });
@ -45,6 +46,7 @@ const immichUser: UserEntity = Object.freeze({
shouldChangePassword: false, shouldChangePassword: false,
profileImagePath: '', profileImagePath: '',
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01',
tags: [], tags: [],
}); });
@ -59,6 +61,7 @@ const updatedImmichUser: UserEntity = Object.freeze({
shouldChangePassword: true, shouldChangePassword: true,
profileImagePath: '', profileImagePath: '',
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01',
tags: [], tags: [],
}); });

View File

@ -48,6 +48,7 @@ const assetResponse: AssetResponseDto = {
resizePath: '', resizePath: '',
createdAt: today.toISOString(), createdAt: today.toISOString(),
modifiedAt: today.toISOString(), modifiedAt: today.toISOString(),
updatedAt: today.toISOString(),
isFavorite: false, isFavorite: false,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
smartInfo: { smartInfo: {
@ -67,6 +68,7 @@ const albumResponse: AlbumResponseDto = {
albumName: 'Test Album', albumName: 'Test Album',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: today.toISOString(), createdAt: today.toISOString(),
updatedAt: today.toISOString(),
id: 'album-123', id: 'album-123',
ownerId: 'admin_id', ownerId: 'admin_id',
sharedUsers: [], sharedUsers: [],
@ -126,6 +128,7 @@ export const userEntityStub = {
shouldChangePassword: false, shouldChangePassword: false,
profileImagePath: '', profileImagePath: '',
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01',
tags: [], tags: [],
}), }),
user1: Object.freeze<UserEntity>({ user1: Object.freeze<UserEntity>({
@ -137,6 +140,7 @@ export const userEntityStub = {
shouldChangePassword: false, shouldChangePassword: false,
profileImagePath: '', profileImagePath: '',
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01',
tags: [], tags: [],
}), }),
}; };
@ -329,6 +333,7 @@ export const sharedLinkStub = {
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
albumName: 'Test Album', albumName: 'Test Album',
createdAt: today.toISOString(), createdAt: today.toISOString(),
updatedAt: today.toISOString(),
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
sharedUsers: [], sharedUsers: [],
sharedLinks: [], sharedLinks: [],
@ -348,6 +353,7 @@ export const sharedLinkStub = {
resizePath: '', resizePath: '',
createdAt: today.toISOString(), createdAt: today.toISOString(),
modifiedAt: today.toISOString(), modifiedAt: today.toISOString(),
updatedAt: today.toISOString(),
isFavorite: false, isFavorite: false,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
smartInfo: { smartInfo: {

View File

@ -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 { AssetAlbumEntity } from './asset-album.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
import { UserAlbumEntity } from './user-album.entity'; import { UserAlbumEntity } from './user-album.entity';
@ -17,6 +17,9 @@ export class AlbumEntity {
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: string; createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
@Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true }) @Column({ comment: 'Asset ID to be used as thumbnail', type: 'varchar', nullable: true })
albumThumbnailAssetId!: string | null; albumThumbnailAssetId!: string | null;

View File

@ -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 { ExifEntity } from './exif.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity'; import { SmartInfoEntity } from './smart-info.entity';
@ -40,6 +50,9 @@ export class AssetEntity {
@Column({ type: 'timestamptz' }) @Column({ type: 'timestamptz' })
modifiedAt!: string; modifiedAt!: string;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
isFavorite!: boolean; isFavorite!: boolean;

View File

@ -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'; import { TagEntity } from './tag.entity';
@Entity('users') @Entity('users')
@ -36,6 +44,9 @@ export class UserEntity {
@DeleteDateColumn() @DeleteDateColumn()
deletedAt?: Date; deletedAt?: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
@OneToMany(() => TagEntity, (tag) => tag.user) @OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[]; tags!: TagEntity[];
} }

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUpdatedAtColumnToAlbumsUsersAssets1675667878312 implements MigrationInterface {
name = 'AddUpdatedAtColumnToAlbumsUsersAssets1675667878312';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -246,6 +246,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'createdAt': string; 'createdAt': string;
/**
*
* @type {string}
* @memberof AlbumResponseDto
*/
'updatedAt': string;
/** /**
* *
* @type {string} * @type {string}
@ -462,6 +468,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto * @memberof AssetResponseDto
*/ */
'modifiedAt': string; 'modifiedAt': string;
/**
*
* @type {string}
* @memberof AssetResponseDto
*/
'updatedAt': string;
/** /**
* *
* @type {boolean} * @type {boolean}

View File

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * 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). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).