浏览代码

chore(server): typeorm definitions fix part 3 (#1796)

* chore(server): tidy up exif typeorm entity definition

* chore(server): tidy up shared link typeorm entity definition

* chore(server): tidy up smart info typeorm entity definition

* chore(server): tidy up tag typeorm entity definition

* ci: add job that checks typeorm migrations are correct and up-to-date
Zack Pollard 2 年之前
父节点
当前提交
d1ea6a897e

+ 45 - 0
.github/workflows/test.yml

@@ -79,6 +79,51 @@ jobs:
           echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
           exit 1
 
+  generated-typeorm-migrations-up-to-date:
+    name: Check generated TypeORM migrations are up-to-date
+    runs-on: ubuntu-latest
+    services:
+      postgres:
+        image: postgres
+        env:
+          POSTGRES_PASSWORD: postgres
+          POSTGRES_USER: postgres
+          POSTGRES_DB: immich
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+        ports:
+          - 5432:5432
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install server dependencies
+        run: |
+          cd server
+          npm ci
+      - name: Run existing migrations
+        run: |
+          cd server
+          npm run typeorm:migrations:run
+      - name: Generate new migrations
+        continue-on-error: true
+        run: |
+          cd server
+          npm run typeorm:migrations:generate ./libs/infra/src/db/migrations/TestMigration
+      - name: Find file changes
+        uses: tj-actions/verify-changed-files@v13.1
+        id: verify-changed-files
+        with:
+          files: |
+            server/libs/infra/src/db/migrations/
+      - name: Verify files have not changed
+        if: steps.verify-changed-files.outputs.files_changed == 'true'
+        run: |
+          echo "ERROR: Generated files not up to date!"
+          echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
+          exit 1
+
   mobile-integration-tests:
     name: Run mobile end-to-end integration tests
     runs-on: macos-latest

+ 0 - 1
mobile/openapi/doc/ExifResponseDto.md

@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
-**id** | **int** |  | [optional] 
 **fileSizeInByte** | **int** |  | [optional] 
 **make** | **String** |  | [optional] 
 **model** | **String** |  | [optional] 

+ 0 - 1
mobile/openapi/doc/SmartInfoResponseDto.md

@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
-**id** | **String** |  | [optional] 
 **tags** | **List<String>** |  | [optional] [default to const []]
 **objects** | **List<String>** |  | [optional] [default to const []]
 

+ 1 - 12
mobile/openapi/lib/model/exif_response_dto.dart

@@ -13,7 +13,6 @@ part of openapi.api;
 class ExifResponseDto {
   /// Returns a new [ExifResponseDto] instance.
   ExifResponseDto({
-    this.id,
     this.fileSizeInByte,
     this.make,
     this.model,
@@ -35,8 +34,6 @@ class ExifResponseDto {
     this.country,
   });
 
-  int? id;
-
   int? fileSizeInByte;
 
   String? make;
@@ -77,7 +74,6 @@ class ExifResponseDto {
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
-     other.id == id &&
      other.fileSizeInByte == fileSizeInByte &&
      other.make == make &&
      other.model == model &&
@@ -101,7 +97,6 @@ class ExifResponseDto {
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
-    (id == null ? 0 : id!.hashCode) +
     (fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
     (make == null ? 0 : make!.hashCode) +
     (model == null ? 0 : model!.hashCode) +
@@ -123,15 +118,10 @@ class ExifResponseDto {
     (country == null ? 0 : country!.hashCode);
 
   @override
-  String toString() => 'ExifResponseDto[id=$id, fileSizeInByte=$fileSizeInByte, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
+  String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
-    if (this.id != null) {
-      json[r'id'] = this.id;
-    } else {
-      // json[r'id'] = null;
-    }
     if (this.fileSizeInByte != null) {
       json[r'fileSizeInByte'] = this.fileSizeInByte;
     } else {
@@ -249,7 +239,6 @@ class ExifResponseDto {
       }());
 
       return ExifResponseDto(
-        id: mapValueOfType<int>(json, r'id'),
         fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
         make: mapValueOfType<String>(json, r'make'),
         model: mapValueOfType<String>(json, r'model'),

+ 1 - 18
mobile/openapi/lib/model/smart_info_response_dto.dart

@@ -13,46 +13,30 @@ part of openapi.api;
 class SmartInfoResponseDto {
   /// Returns a new [SmartInfoResponseDto] instance.
   SmartInfoResponseDto({
-    this.id,
     this.tags = const [],
     this.objects = const [],
   });
 
-  ///
-  /// 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.
-  ///
-  String? id;
-
   List<String>? tags;
 
   List<String>? objects;
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is SmartInfoResponseDto &&
-     other.id == id &&
      other.tags == tags &&
      other.objects == objects;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
-    (id == null ? 0 : id!.hashCode) +
     (tags == null ? 0 : tags!.hashCode) +
     (objects == null ? 0 : objects!.hashCode);
 
   @override
-  String toString() => 'SmartInfoResponseDto[id=$id, tags=$tags, objects=$objects]';
+  String toString() => 'SmartInfoResponseDto[tags=$tags, objects=$objects]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
-    if (this.id != null) {
-      json[r'id'] = this.id;
-    } else {
-      // json[r'id'] = null;
-    }
     if (this.tags != null) {
       json[r'tags'] = this.tags;
     } else {
@@ -85,7 +69,6 @@ class SmartInfoResponseDto {
       }());
 
       return SmartInfoResponseDto(
-        id: mapValueOfType<String>(json, r'id'),
         tags: json[r'tags'] is List
             ? (json[r'tags'] as List).cast<String>()
             : const [],

+ 0 - 5
mobile/openapi/test/exif_response_dto_test.dart

@@ -16,11 +16,6 @@ void main() {
   // final instance = ExifResponseDto();
 
   group('test ExifResponseDto', () {
-    // int id
-    test('to test the property `id`', () async {
-      // TODO
-    });
-
     // int fileSizeInByte
     test('to test the property `fileSizeInByte`', () async {
       // TODO

+ 0 - 5
mobile/openapi/test/smart_info_response_dto_test.dart

@@ -16,11 +16,6 @@ void main() {
   // final instance = SmartInfoResponseDto();
 
   group('test SmartInfoResponseDto', () {
-    // String id
-    test('to test the property `id`', () async {
-      // TODO
-    });
-
     // List<String> tags (default value: const [])
     test('to test the property `tags`', () async {
       // TODO

+ 1 - 1
server/apps/microservices/src/processors/metadata-extraction.processor.ts

@@ -253,7 +253,7 @@ export class MetadataExtractionProcessor {
     if (this.isGeocodeInitialized) {
       const { latitude, longitude } = job.data;
       const { country, state, city } = await this.reverseGeocodeExif(latitude, longitude);
-      await this.exifRepository.update({ id: job.data.exifId }, { city, state, country });
+      await this.exifRepository.update({ assetId: job.data.assetId }, { city, state, country });
     }
   }
 

+ 0 - 9
server/immich-openapi-specs.json

@@ -3136,12 +3136,6 @@
       "ExifResponseDto": {
         "type": "object",
         "properties": {
-          "id": {
-            "type": "integer",
-            "nullable": true,
-            "default": null,
-            "format": "int64"
-          },
           "fileSizeInByte": {
             "type": "integer",
             "nullable": true,
@@ -3245,9 +3239,6 @@
       "SmartInfoResponseDto": {
         "type": "object",
         "properties": {
-          "id": {
-            "type": "string"
-          },
           "tags": {
             "nullable": true,
             "type": "array",

+ 0 - 3
server/libs/domain/src/asset/response-dto/exif-response.dto.ts

@@ -2,8 +2,6 @@ import { ExifEntity } from '@app/infra/db/entities';
 import { ApiProperty } from '@nestjs/swagger';
 
 export class ExifResponseDto {
-  @ApiProperty({ type: 'integer', format: 'int64' })
-  id?: number | null = null;
   make?: string | null = null;
   model?: string | null = null;
   imageName?: string | null = null;
@@ -29,7 +27,6 @@ export class ExifResponseDto {
 
 export function mapExif(entity: ExifEntity): ExifResponseDto {
   return {
-    id: entity.id,
     make: entity.make,
     model: entity.model,
     imageName: entity.imageName,

+ 0 - 2
server/libs/domain/src/asset/response-dto/smart-info-response.dto.ts

@@ -1,14 +1,12 @@
 import { SmartInfoEntity } from '@app/infra/db/entities';
 
 export class SmartInfoResponseDto {
-  id?: string;
   tags?: string[] | null;
   objects?: string[] | null;
 }
 
 export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
   return {
-    id: entity.id,
     tags: entity.tags,
     objects: entity.objects,
   };

+ 1 - 1
server/libs/domain/src/job/interfaces/metadata-extraction.interface.ts

@@ -25,7 +25,7 @@ export interface IVideoLengthExtractionProcessor {
 }
 
 export interface IReverseGeocodingProcessor {
-  exifId: number;
+  assetId: string;
   latitude: number;
   longitude: number;
 }

+ 0 - 4
server/libs/domain/test/fixtures.ts

@@ -119,7 +119,6 @@ export const assetEntityStub = {
 };
 
 const assetInfo: ExifResponseDto = {
-  id: 1,
   make: 'camera-make',
   model: 'camera-model',
   imageName: 'fancy-image',
@@ -155,7 +154,6 @@ const assetResponse: AssetResponseDto = {
   isFavorite: false,
   mimeType: 'image/jpeg',
   smartInfo: {
-    id: 'should-be-a-number',
     tags: [],
     objects: ['a', 'b', 'c'],
   },
@@ -391,7 +389,6 @@ export const sharedLinkStub = {
           isFavorite: false,
           mimeType: 'image/jpeg',
           smartInfo: {
-            id: 'should-be-a-number',
             assetId: 'id_1',
             tags: [],
             objects: ['a', 'b', 'c'],
@@ -405,7 +402,6 @@ export const sharedLinkStub = {
           livePhotoVideoId: null,
           exifInfo: {
             livePhotoCID: null,
-            id: 1,
             assetId: 'id_1',
             description: 'description',
             exifImageWidth: 500,

+ 6 - 11
server/libs/infra/src/db/entities/exif.entity.ts

@@ -1,20 +1,19 @@
-import { Index, JoinColumn, OneToOne } from 'typeorm';
+import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
 import { Column } from 'typeorm/decorator/columns/Column';
-import { PrimaryGeneratedColumn } from 'typeorm/decorator/columns/PrimaryGeneratedColumn';
 import { Entity } from 'typeorm/decorator/entity/Entity';
 import { AssetEntity } from './asset.entity';
 
 @Entity('exif')
 export class ExifEntity {
-  @PrimaryGeneratedColumn()
-  id!: number;
+  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
+  @JoinColumn()
+  asset?: AssetEntity;
 
-  @Index({ unique: true })
-  @Column({ type: 'uuid' })
+  @PrimaryColumn()
   assetId!: string;
 
   /* General info */
-  @Column({ type: 'text', nullable: true, default: '' })
+  @Column({ type: 'text', default: '' })
   description!: string; // or caption
 
   @Column({ type: 'integer', nullable: true })
@@ -83,10 +82,6 @@ export class ExifEntity {
   @Column({ type: 'float8', nullable: true })
   fps?: number | null;
 
-  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
-  @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
-  asset?: AssetEntity;
-
   @Index('exif_text_searchable', { synchronize: false })
   @Column({
     type: 'tsvector',

+ 11 - 4
server/libs/infra/src/db/entities/shared-link.entity.ts

@@ -1,4 +1,13 @@
-import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
+import {
+  Column,
+  CreateDateColumn,
+  Entity,
+  Index,
+  ManyToMany,
+  ManyToOne,
+  PrimaryGeneratedColumn,
+  Unique,
+} from 'typeorm';
 import { AlbumEntity } from './album.entity';
 import { AssetEntity } from './asset.entity';
 import { UserEntity } from './user.entity';
@@ -25,7 +34,7 @@ export class SharedLinkEntity {
   @Column()
   type!: SharedLinkType;
 
-  @Column({ type: 'timestamptz' })
+  @CreateDateColumn({ type: 'timestamptz' })
   createdAt!: string;
 
   @Column({ type: 'timestamptz', nullable: true })
@@ -56,5 +65,3 @@ export enum SharedLinkType {
    */
   INDIVIDUAL = 'INDIVIDUAL',
 }
-
-// npm run typeorm -- migration:generate ./libs/infra/src/db/AddMorePermissionToSharedLink -d ./libs/infra/src/db/config/database.config.ts

+ 5 - 9
server/libs/infra/src/db/entities/smart-info.entity.ts

@@ -1,13 +1,13 @@
-import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
 import { AssetEntity } from './asset.entity';
 
 @Entity('smart_info')
 export class SmartInfoEntity {
-  @PrimaryGeneratedColumn()
-  id!: string;
+  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
+  @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
+  asset?: AssetEntity;
 
-  @Index({ unique: true })
-  @Column({ type: 'uuid' })
+  @PrimaryColumn()
   assetId!: string;
 
   @Column({ type: 'text', array: true, nullable: true })
@@ -15,8 +15,4 @@ export class SmartInfoEntity {
 
   @Column({ type: 'text', array: true, nullable: true })
   objects!: string[] | null;
-
-  @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
-  @JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
-  asset?: AssetEntity;
 }

+ 3 - 3
server/libs/infra/src/db/entities/tag.entity.ts

@@ -14,6 +14,9 @@ export class TagEntity {
   @Column()
   name!: string;
 
+  @ManyToOne(() => UserEntity, (user) => user.tags)
+  user!: UserEntity;
+
   @Column()
   userId!: string;
 
@@ -22,9 +25,6 @@ export class TagEntity {
 
   @ManyToMany(() => AssetEntity, (asset) => asset.tags)
   assets!: AssetEntity[];
-
-  @ManyToOne(() => UserEntity, (user) => user.tags)
-  user!: UserEntity;
 }
 
 export enum TagType {

+ 28 - 0
server/libs/infra/src/db/migrations/1676848629119-ExifEntityDefinitionFixes.ts

@@ -0,0 +1,28 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ExifEntityDefinitionFixes1676848629119 implements MigrationInterface {
+    name = 'ExifEntityDefinitionFixes1676848629119'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" SET NOT NULL`);
+
+        await queryRunner.query(`DROP INDEX "public"."IDX_c0117fdbc50b917ef9067740c4"`);
+        await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_28663352d85078ad0046dafafaa"`);
+        await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "id"`);
+        await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`);
+        await queryRunner.query(`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId")`);
+        await queryRunner.query(`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" DROP NOT NULL`);
+
+        await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`);
+        await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_c0117fdbc50b917ef9067740c44"`);
+        await queryRunner.query(`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "exif" ADD "id" SERIAL NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "exif" ADD CONSTRAINT "PK_28663352d85078ad0046dafafaa" PRIMARY KEY ("id")`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c0117fdbc50b917ef9067740c4" ON "exif" ("assetId") `);
+    }
+
+}

+ 14 - 0
server/libs/infra/src/db/migrations/1676848694786-SharedLinkEntityDefinitionFixes.ts

@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class SharedLinkEntityDefinitionFixes1676848694786 implements MigrationInterface {
+    name = 'SharedLinkEntityDefinitionFixes1676848694786'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "createdAt" SET DEFAULT now()`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "shared_links" ALTER COLUMN "createdAt" DROP DEFAULT`);
+    }
+
+}

+ 24 - 0
server/libs/infra/src/db/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts

@@ -0,0 +1,24 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class SmartInfoEntityDefinitionFixes1676852143506 implements MigrationInterface {
+    name = 'SmartInfoEntityDefinitionFixes1676852143506'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP INDEX "public"."IDX_5e3753aadd956110bf3ec0244a"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_0beace66440e9713f5c40470e46"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN "id"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" ADD CONSTRAINT "PK_5e3753aadd956110bf3ec0244ac" PRIMARY KEY ("assetId")`);
+        await queryRunner.query(`ALTER TABLE "smart_info" ADD CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_5e3753aadd956110bf3ec0244ac"`);
+        await queryRunner.query(`ALTER TABLE "smart_info" ADD CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "smart_info" ADD "id" SERIAL NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "smart_info" ADD CONSTRAINT "PK_0beace66440e9713f5c40470e46" PRIMARY KEY ("id")`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5e3753aadd956110bf3ec0244a" ON "smart_info" ("assetId") `);
+    }
+
+}

+ 0 - 12
web/src/api/open-api/api.ts

@@ -1054,12 +1054,6 @@ export interface EditSharedLinkDto {
  * @interface ExifResponseDto
  */
 export interface ExifResponseDto {
-    /**
-     * 
-     * @type {number}
-     * @memberof ExifResponseDto
-     */
-    'id'?: number | null;
     /**
      * 
      * @type {number}
@@ -1730,12 +1724,6 @@ export interface SignUpDto {
  * @interface SmartInfoResponseDto
  */
 export interface SmartInfoResponseDto {
-    /**
-     * 
-     * @type {string}
-     * @memberof SmartInfoResponseDto
-     */
-    'id'?: string;
     /**
      * 
      * @type {Array<string>}