From df60950e568a72c0d7280f304a83772b241afae6 Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Thu, 24 Oct 2024 08:37:52 +0200 Subject: [PATCH] Feature: delete asset within edit form --- apps/client-asset-sg/src/app/i18n/de.ts | 3 + apps/client-asset-sg/src/app/i18n/en.ts | 3 + apps/client-asset-sg/src/app/i18n/fr.ts | 3 + apps/client-asset-sg/src/app/i18n/it.ts | 3 + apps/client-asset-sg/src/app/i18n/rm.ts | 3 + apps/server-asset-sg/src/app.module.ts | 2 - .../asset-edit/asset-edit.controller.ts | 53 +++-- .../features/asset-edit/asset-edit.fake.ts | 7 +- .../asset-edit/asset-edit.repo.spec.ts | 4 +- .../features/asset-edit/asset-edit.repo.ts | 95 +++++---- .../features/asset-edit/asset-edit.service.ts | 41 ---- .../src/features/assets/assets.http | 195 +++++++++--------- .../search/asset-search.service.spec.ts | 31 ++- .../assets/search/asset-search.service.ts | 8 + .../src/features/files/file.repo.ts | 2 +- e2e/cypress/e2e/create-asset/create-asset.ts | 2 +- .../e2e/edit-asset/edit-asset-steps.ts | 2 +- ...t-editor-tab-administration.component.html | 21 +- ...t-editor-tab-administration.component.scss | 6 + ...set-editor-tab-administration.component.ts | 24 ++- .../asset-editor-tab-page.component.html | 8 +- .../asset-editor-tab-page.component.ts | 5 + .../src/lib/services/asset-editor.service.ts | 10 +- .../src/lib/state/asset-editor.actions.ts | 3 + .../src/lib/state/asset-editor.effects.ts | 38 +++- .../asset-search/asset-search.effects.ts | 16 +- .../confirm-dialog.component.html | 9 + .../confirm-dialog.component.scss | 6 + .../confirm-dialog.component.ts | 53 +++++ .../lib/components/confirm-dialog/index.ts | 1 + .../client-shared/src/lib/components/index.ts | 1 + .../src/lib/state/app-shared-state.actions.ts | 7 + .../migration.sql | 113 ++++++++++ libs/persistence/prisma/schema.prisma | 40 ++-- 34 files changed, 554 insertions(+), 264 deletions(-) delete mode 100644 apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts create mode 100644 libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.html create mode 100644 libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.scss create mode 100644 libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.ts create mode 100644 libs/client-shared/src/lib/components/confirm-dialog/index.ts create mode 100644 libs/persistence/prisma/migrations/20241024140253_delete_asset_cascade/migration.sql diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index ea4a083b..4208803b 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -6,6 +6,9 @@ export const deAppTranslations = { ok: 'OK', submit: 'Absenden', cancel: 'Abbrechen', + confirm: 'Bestätigen', + confirmDelete: 'Sind Sie sicher, dass Sie dieses Asset löschen wollen?', + deleteSuccess: 'Das Asset wurde erfolgreich gelöscht.', login: 'Anmelden', logout: 'Abmelden', yes: 'Ja', diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index 30bb7a74..96d50d6e 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -8,6 +8,9 @@ export const enAppTranslations: AppTranslations = { ok: 'OK', submit: 'Submit', cancel: 'Cancel', + confirm: 'Confirm', + confirmDelete: 'Are you sure you want to delete this asset?', + deleteSuccess: 'The asset was successfully deleted.', login: 'Login', logout: 'Logout', yes: 'Yes', diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index 29114e0b..7d398311 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -8,6 +8,9 @@ export const frAppTranslations: AppTranslations = { ok: 'OK', submit: 'Envoyer', cancel: 'Annuler', + confirm: 'Confirmer', + confirmDelete: 'Êtes-vous sûr de vouloir supprimer cet asset', + deleteSuccess: "L'asset a été supprimé avec succès.", login: 'Login', logout: 'Déconnecter', yes: 'Oui', diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 80783047..b49f2daa 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -8,6 +8,9 @@ export const itAppTranslations: AppTranslations = { ok: 'OK', submit: 'IT Absenden', cancel: 'IT Abbrechen', + confirm: 'IT Bestätigen', + confirmDelete: 'IT Sind Sie sicher, dass Sie dieses Asset löschen möchten?', + deleteSuccess: 'IT Das Asset wurde erfolgreich gelöscht.', login: 'Login', logout: 'IT Abmelden', yes: 'Sì', diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index 193a99cf..e81baeb6 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -8,6 +8,9 @@ export const rmAppTranslations: AppTranslations = { ok: 'OK', submit: 'RM Absenden', cancel: 'RM Abbrechen', + confirm: 'RM Bestätigen', + confirmDelete: 'RM Sind Sie sicher, dass Sie dieses Asset löschen möchten?', + deleteSuccess: 'RM Das Asset wurde erfolgreich gelöscht.', login: 'Login', logout: 'RM Abmelden', yes: 'Sì', diff --git a/apps/server-asset-sg/src/app.module.ts b/apps/server-asset-sg/src/app.module.ts index 9fd76b90..2512ceb7 100644 --- a/apps/server-asset-sg/src/app.module.ts +++ b/apps/server-asset-sg/src/app.module.ts @@ -11,7 +11,6 @@ import { JwtMiddleware } from '@/core/middleware/jwt.middleware'; import { PrismaService } from '@/core/prisma.service'; import { AssetEditController } from '@/features/asset-edit/asset-edit.controller'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; -import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; import { AssetInfoRepo } from '@/features/assets/asset-info.repo'; import { AssetRepo } from '@/features/assets/asset.repo'; import { AssetsController } from '@/features/assets/assets.controller'; @@ -51,7 +50,6 @@ import { WorkgroupsController } from '@/features/workgroups/workgroups.controlle providers: [ provideElasticsearch, AssetEditRepo, - AssetEditService, AssetInfoRepo, AssetRepo, AssetSearchService, diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts index fb29d153..f505b30d 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts @@ -1,17 +1,27 @@ -import { PatchAsset } from '@asset-sg/shared'; +import { AssetEditDetail, PatchAsset } from '@asset-sg/shared'; import { AssetEditPolicy, Role, User } from '@asset-sg/shared/v2'; -import { Controller, Get, HttpException, HttpStatus, Param, ParseIntPipe, Post, Put } from '@nestjs/common'; -import * as E from 'fp-ts/Either'; +import { + Controller, + Delete, + Get, + HttpCode, + HttpException, + HttpStatus, + Param, + ParseIntPipe, + Post, + Put, +} from '@nestjs/common'; import * as O from 'fp-ts/Option'; import { authorize } from '@/core/authorize'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { ParseBody } from '@/core/decorators/parse.decorator'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; -import { AssetEditDetail, AssetEditService } from '@/features/asset-edit/asset-edit.service'; +import { AssetSearchService } from '@/features/assets/search/asset-search.service'; @Controller('/asset-edit') export class AssetEditController { - constructor(private readonly assetEditRepo: AssetEditRepo, private readonly assetEditService: AssetEditService) {} + constructor(private readonly assetEditRepo: AssetEditRepo, private readonly assetSearchService: AssetSearchService) {} @Get('/:id') async show(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { @@ -27,11 +37,10 @@ export class AssetEditController { async create(@ParseBody(PatchAsset) patch: PatchAsset, @CurrentUser() user: User) { authorize(AssetEditPolicy, user).canCreate(); validatePatch(user, patch); - const result = await this.assetEditService.createAsset(user, patch)(); - if (E.isLeft(result)) { - throw new HttpException(result.left.message, 500); - } - return result.right; + + const asset = await this.assetEditRepo.create({ user, patch }); + await this.assetSearchService.register(asset); + return AssetEditDetail.encode(asset); } @Put('/:id') @@ -48,11 +57,27 @@ export class AssetEditController { authorize(AssetEditPolicy, user).canUpdate(record); validatePatch(user, patch, record); - const result = await this.assetEditService.updateAsset(user, record.assetId, patch)(); - if (E.isLeft(result)) { - throw new HttpException(result.left.message, 500); + const asset = await this.assetEditRepo.update(record.assetId, { user, patch }); + if (asset === null) { + throw new HttpException('not found', 404); + } + await this.assetSearchService.register(asset); + return AssetEditDetail.encode(asset); + } + + @Delete('/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async delete(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { + const record = await this.assetEditRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetEditPolicy, user).canDelete(record); + const success = await this.assetEditRepo.delete(record.assetId); + if (!success) { + throw new HttpException('could not delete', HttpStatus.INTERNAL_SERVER_ERROR); } - return result.right; + await this.assetSearchService.deleteFromIndex(record.assetId); } } diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts index 46387787..76e25576 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts @@ -1,6 +1,5 @@ -import { AssetUsage, Contact, PatchAsset, dateIdFromDate } from '@asset-sg/shared'; -import { User, WorkgroupId } from '@asset-sg/shared/v2'; -import { Role } from '@asset-sg/shared/v2'; +import { AssetEditDetail, AssetUsage, Contact, dateIdFromDate, PatchAsset } from '@asset-sg/shared'; +import { Role, User, WorkgroupId } from '@asset-sg/shared/v2'; import { fakerDE_CH as faker } from '@faker-js/faker'; import * as O from 'fp-ts/Option'; @@ -11,8 +10,6 @@ import { fakeAssetKindItemCode } from '../../../../../test/data/asset-kind-item' // eslint-disable-next-line @nx/enforce-module-boundaries import { fakeContactKindItem } from '../../../../../test/data/contact-kind-item'; -import { AssetEditDetail } from './asset-edit.service'; - import { define } from '@/utils/define'; let nextUniqueId = 0; diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts index 913a66ea..c567a73e 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts @@ -7,10 +7,12 @@ import { fakeAssetPatch, fakeUser } from './asset-edit.fake'; import { AssetEditRepo } from './asset-edit.repo'; import { PrismaService } from '@/core/prisma.service'; +import { FileRepo } from '@/features/files/file.repo'; describe(AssetEditRepo, () => { const prisma = new PrismaService(); - const repo = new AssetEditRepo(prisma); + const fileRepo = new FileRepo(prisma); + const repo = new AssetEditRepo(prisma, fileRepo); beforeAll(async () => { await setupDB(prisma); diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts index e03cd953..8aa98832 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts @@ -1,15 +1,14 @@ import { decodeError, isNotNull } from '@asset-sg/core'; -import { AssetUsage, dateFromDateId, DateIdFromDate, PatchAsset } from '@asset-sg/shared'; +import { AssetEditDetail, AssetUsage, dateFromDateId, DateIdFromDate, PatchAsset } from '@asset-sg/shared'; import { User } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import * as E from 'fp-ts/Either'; import * as O from 'fp-ts/Option'; -import { AssetEditDetail } from './asset-edit.service'; - import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; +import { FileRepo } from '@/features/files/file.repo'; import { AssetEditDetailFromPostgres } from '@/models/asset-edit-detail'; import { createStudies, @@ -17,10 +16,11 @@ import { postgresStudiesByAssetId, updateStudies, } from '@/utils/postgres-studies/postgres-studies'; +import { handlePrismaMutationError } from '@/utils/prisma'; @Injectable() export class AssetEditRepo implements Repo { - constructor(private readonly prismaService: PrismaService) {} + constructor(private readonly prismaService: PrismaService, private readonly fileRepo: FileRepo) {} async find(id: number): Promise { const asset = await this.prismaService.asset.findUnique({ @@ -256,55 +256,52 @@ export class AssetEditRepo implements Repo { - // Delete the record's `manCatLabelRef` records. - await this.prismaService.manCatLabelRef.deleteMany({ - where: { assetId: id }, - }); - - // Delete the record's `assetContact` records. - await this.prismaService.assetContact.deleteMany({ - where: { assetId: id }, - }); - - // Delete the record's `assetLanguage` records. - await this.prismaService.assetLanguage.deleteMany({ - where: { assetId: id }, - }); - - // Delete the record's `id` records. - await this.prismaService.id.deleteMany({ - where: { assetId: id }, - }); - - // Delete the record's `typeNatRel` records. - await this.prismaService.typeNatRel.deleteMany({ - where: { assetId: id }, - }); - - // Delete the record's `statusWork` records. - await this.prismaService.statusWork.deleteMany({ - where: { assetId: id }, - }); + try { + await this.prismaService.$transaction(async () => { + // Delete the record's `file` records. + const assetFileIds = await this.prismaService.assetFile.findMany({ + where: { assetId: id }, + select: { fileId: true }, + }); + + for (const { fileId } of assetFileIds) { + // Placeholder email + await this.fileRepo.delete({ id: fileId, assetId: id, user: { email: 'email@mail.com' } }); + } + + // Delete the record. + await this.prismaService.asset.delete({ where: { assetId: id } }); + + // Delete all `internalUse` records that are not in use anymore. + await this.prismaService.internalUse.deleteMany({ + where: { + Asset: { none: {} }, + }, + }); - // Delete the record. - await this.prismaService.asset.delete({ where: { assetId: id } }); + // Delete all `publicUse` records that are not in use anymore. + await this.prismaService.publicUse.deleteMany({ + where: { + Asset: { none: {} }, + }, + }); - // Delete all `internalUse` records that are not in use anymore. - await this.prismaService.internalUse.deleteMany({ - where: { - Asset: { none: {} }, - }, - }); + await this.prismaService.assetFormatItem.deleteMany({ + where: { + assets: { none: {} }, + }, + }); - // Delete all `publicUse` records that are not in use anymore. - await this.prismaService.publicUse.deleteMany({ - where: { - Asset: { none: {} }, - }, + await this.prismaService.assetKindItem.deleteMany({ + where: { + assets: { none: {} }, + }, + }); }); - }); - return true; + return true; + } catch (e) { + return handlePrismaMutationError(e) ?? false; + } } private async loadDetail(asset: PrismaAsset): Promise { diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts deleted file mode 100644 index 21d2ab25..00000000 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { isNotNull, unknownToUnknownError } from '@asset-sg/core'; -import { BaseAssetEditDetail, PatchAsset } from '@asset-sg/shared'; -import { User } from '@asset-sg/shared/v2'; -import { Injectable } from '@nestjs/common'; -import { pipe } from 'fp-ts/function'; -import * as TE from 'fp-ts/TaskEither'; -import * as C from 'io-ts/Codec'; - -import { AssetEditRepo } from './asset-edit.repo'; -import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { notFoundError } from '@/utils/errors'; - -export const AssetEditDetail = C.struct({ - ...BaseAssetEditDetail, - studies: C.array(C.struct({ assetId: C.number, studyId: C.string, geomText: C.string })), -}); -export type AssetEditDetail = C.TypeOf; - -@Injectable() -export class AssetEditService { - constructor(private readonly assetEditRepo: AssetEditRepo, private readonly assetSearchService: AssetSearchService) {} - - public createAsset(user: User, patch: PatchAsset) { - return pipe( - TE.tryCatch(() => this.assetEditRepo.create({ user, patch }), unknownToUnknownError), - TE.chain(({ assetId }) => TE.tryCatch(() => this.assetEditRepo.find(assetId), unknownToUnknownError)), - TE.chainW(TE.fromPredicate(isNotNull, notFoundError)), - TE.tap((asset) => TE.tryCatch(() => this.assetSearchService.register(asset), unknownToUnknownError)), - TE.map((asset) => AssetEditDetail.encode(asset)) - ); - } - - public updateAsset(user: User, assetId: number, patch: PatchAsset) { - return pipe( - TE.tryCatch(() => this.assetEditRepo.update(assetId, { user, patch }), unknownToUnknownError), - TE.chainW(TE.fromPredicate(isNotNull, notFoundError)), - TE.tap((asset) => TE.tryCatch(() => this.assetSearchService.register(asset), unknownToUnknownError)), - TE.map((asset) => AssetEditDetail.encode(asset)) - ); - } -} diff --git a/apps/server-asset-sg/src/features/assets/assets.http b/apps/server-asset-sg/src/features/assets/assets.http index d248a244..11eea6ef 100644 --- a/apps/server-asset-sg/src/features/assets/assets.http +++ b/apps/server-asset-sg/src/features/assets/assets.http @@ -1,96 +1,99 @@ -### Create asset -POST {{host}}/api/assets -Authorization: Impersonate {{user}} -Content-Type: application/json - -{ - "title": "My new Asset", - "originalTitle": null, - "municipality": null, - "kindCode": "package", - "formatCode": "binary", - "languageCodes": [], - "manCatLabelCodes": [], - "natRelCodes": [], - "isNatRel": false, - "links": { - "parent": null, - "siblings": [ - 44382 - ] - }, - "identifiers": [], - "statuses": [], - "studies": [], - "contactAssignments": [], - "createdAt": "2024-02-01", - "receivedAt": "2024-02-01", - "infoGeol": { - "main": null, - "contact": null, - "auxiliary": null - }, - "sgsId": null, - "usage": { - "public": { - "isAvailable": true, - "statusCode": "tobechecked" - }, - "internal": { - "isAvailable": true, - "statusCode": "approved" - } - }, - "workgroupId": 4 -} - -### Update asset -PUT {{host}}/api/assets/44383 -Authorization: Impersonate {{user}} -Content-Type: application/json - -{ - "title": "My Title", - "originalTitle": null, - "municipality": null, - "kindCode": "package", - "formatCode": "binary", - "languageCodes": [], - "manCatLabelCodes": [], - "natRelCodes": [], - "isNatRel": false, - "links": { - "parent": 44382, - "siblings": [ - 44382, - 44384 - ] - }, - "identifiers": [], - "statuses": [], - "studies": [], - "contactAssignments": [], - "createdAt": "2024-02-01", - "receivedAt": "2024-02-01", - "infoGeol": { - "main": null, - "contact": null, - "auxiliary": null - }, - "sgsId": null, - "usage": { - "public": { - "isAvailable": true, - "statusCode": "tobechecked" - }, - "internal": { - "isAvailable": true, - "statusCode": "approved" - } - }, - "workgroupId": 1 -} - -### Get asset -GET {{host}}/api/assets/44383 -Authorization: Impersonate {{user}} +### Create asset +POST {{host}}/api/assets +Authorization: Impersonate {{user}} +Content-Type: application/json + +{ + "title": "My new Asset", + "originalTitle": null, + "municipality": null, + "kindCode": "package", + "formatCode": "binary", + "languageCodes": [], + "manCatLabelCodes": [], + "natRelCodes": [], + "isNatRel": false, + "links": { + "parent": null, + "siblings": [] + }, + "identifiers": [], + "statuses": [], + "studies": [], + "contactAssignments": [], + "createdAt": "2024-02-01", + "receivedAt": "2024-02-01", + "infoGeol": { + "main": null, + "contact": null, + "auxiliary": null + }, + "sgsId": null, + "usage": { + "public": { + "isAvailable": true, + "statusCode": "tobechecked" + }, + "internal": { + "isAvailable": true, + "statusCode": "tobechecked" + } + }, + "workgroupId": 2 +} + +### Update asset +PUT {{host}}/api/assets/44383 +Authorization: Impersonate {{user}} +Content-Type: application/json + +{ + "title": "My Title", + "originalTitle": null, + "municipality": null, + "kindCode": "package", + "formatCode": "binary", + "languageCodes": [], + "manCatLabelCodes": [], + "natRelCodes": [], + "isNatRel": false, + "links": { + "parent": 44382, + "siblings": [ + 44382, + 44384 + ] + }, + "identifiers": [], + "statuses": [], + "studies": [], + "contactAssignments": [], + "createdAt": "2024-02-01", + "receivedAt": "2024-02-01", + "infoGeol": { + "main": null, + "contact": null, + "auxiliary": null + }, + "sgsId": null, + "usage": { + "public": { + "isAvailable": true, + "statusCode": "tobechecked" + }, + "internal": { + "isAvailable": true, + "statusCode": "approved" + } + }, + "workgroupId": 1 +} + +### Get asset +GET {{host}}/api/assets/44383 +Authorization: Impersonate {{user}} + + +### Delete asset +DELETE {{host}}/api/assets/44382 +Authorization: Impersonate {{user}} diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts index f6486124..3a9aa4c3 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts @@ -1,16 +1,17 @@ import { + AssetEditDetail, AssetSearchResult, AssetSearchStats, + dateFromDateId, + dateIdFromDate, ElasticSearchAsset, + makeUsageCode, PageStats, PatchAsset, SearchAsset, SearchAssetAggregations, SearchAssetResultNonEmpty, UsageCode, - dateFromDateId, - dateIdFromDate, - makeUsageCode, } from '@asset-sg/shared'; import { faker } from '@faker-js/faker'; @@ -31,13 +32,14 @@ import { openElasticsearchClient } from '@/core/elasticsearch'; import { PrismaService } from '@/core/prisma.service'; import { fakeAssetPatch, fakeAssetUsage, fakeContact, fakeUser } from '@/features/asset-edit/asset-edit.fake'; import { AssetEditData, AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; -import { AssetEditDetail } from '@/features/asset-edit/asset-edit.service'; +import { FileRepo } from '@/features/files/file.repo'; import { StudyRepo } from '@/features/studies/study.repo'; describe(AssetSearchService, () => { const elastic = openElasticsearchClient(); const prisma = new PrismaService(); - const assetRepo = new AssetEditRepo(prisma); + const fileRepo = new FileRepo(prisma); + const assetRepo = new AssetEditRepo(prisma, fileRepo); const studyRepo = new StudyRepo(prisma); const service = new AssetSearchService(elastic, prisma, assetRepo, studyRepo); @@ -107,6 +109,25 @@ describe(AssetSearchService, () => { }); }); + describe('deleteFromIndex', () => { + it('deletes an an asset from elastic search', async () => { + // Given + const asset = await assetRepo.create({ patch: fakeAssetPatch(), user: fakeUser() }); + await service.register(asset); + + // When + await service.deleteFromIndex(asset.assetId); + + const response = await elastic.search({ + index: ASSET_ELASTIC_INDEX, + size: 10_000, + _source: true, + }); + //Then + expect(response.hits.hits.length).toEqual(0); + }); + }); + describe('search', () => { const assertSingleResult = (result: AssetSearchResult, asset: AssetEditDetail): void => { expect(result.page).toEqual({ total: 1, size: 1, offset: 0 } as PageStats); diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts index 1b363bcd..40d4f4a5 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts @@ -64,6 +64,14 @@ export class AssetSearchService { return this.registerWithOptions(oneOrMore, { index: INDEX, shouldRefresh: true }); } + async deleteFromIndex(assetId: number): Promise { + await this.elastic.delete({ + index: INDEX, + id: `${assetId}`, + refresh: true, + }); + } + async count(): Promise { return (await this.elastic.count({ index: INDEX, ignore_unavailable: true })).count; } diff --git a/apps/server-asset-sg/src/features/files/file.repo.ts b/apps/server-asset-sg/src/features/files/file.repo.ts index 53df003e..162ca1a6 100644 --- a/apps/server-asset-sg/src/features/files/file.repo.ts +++ b/apps/server-asset-sg/src/features/files/file.repo.ts @@ -115,7 +115,7 @@ interface CreateFileData { interface DeleteFileData { id: number; assetId: AssetId; - user: User; + user: Pick; } export const determineUniqueFilename = async ( diff --git a/e2e/cypress/e2e/create-asset/create-asset.ts b/e2e/cypress/e2e/create-asset/create-asset.ts index f66ff385..82c7195b 100644 --- a/e2e/cypress/e2e/create-asset/create-asset.ts +++ b/e2e/cypress/e2e/create-asset/create-asset.ts @@ -52,7 +52,7 @@ When(/^The user fills out administration information$/, () => { }); When(/^The user clicks the save button$/, () => { cy.intercept('http://localhost:4200/api/asset-edit').as('save'); - cy.get('button:contains(" Speichern ")').click(); + cy.get('button:contains("Speichern")').click(); }); Then(/^The user should see the Create Asset form$/, () => { cy.get('asset-sg-editor-tab-page').should('be.visible'); diff --git a/e2e/cypress/e2e/edit-asset/edit-asset-steps.ts b/e2e/cypress/e2e/edit-asset/edit-asset-steps.ts index da00d207..c9be64e2 100644 --- a/e2e/cypress/e2e/edit-asset/edit-asset-steps.ts +++ b/e2e/cypress/e2e/edit-asset/edit-asset-steps.ts @@ -22,7 +22,7 @@ When(/^The user edits asset information$/, () => { When(/^The user clicks save$/, () => { cy.intercept('http://localhost:4200/api/asset-edit/**').as('edit'); cy.get('button:contains("Administration")').click(); - cy.get('button:contains(" Speichern ")').click(); + cy.get('button:contains("Speichern")').click(); cy.wait(1000); }); Then(/^The changes are saved$/, () => { diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html index 36ab673a..4132aa3f 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html @@ -106,15 +106,18 @@ - +
+ + +
diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.scss b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.scss index cbd86ae4..ef656e69 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.scss +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.scss @@ -17,3 +17,9 @@ padding-left: 3rem; padding-right: 3rem; } + +.actions { + margin-top: 0.5rem; + display: flex; + gap: 12px; +} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts index 613c1c2e..db42bd80 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts @@ -1,10 +1,12 @@ import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; import { FormGroupDirective } from '@angular/forms'; -import { fromAppShared } from '@asset-sg/client-shared'; +import { MatDialog } from '@angular/material/dialog'; +import { ConfirmDialogComponent, fromAppShared } from '@asset-sg/client-shared'; import { isNotNull } from '@asset-sg/core'; import { DateId } from '@asset-sg/shared'; import { isMasterEditor } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; import * as O from 'fp-ts/Option'; @@ -28,6 +30,7 @@ const initialTabAdministrationState: TabAdministrationState = { assetEditDetail: O.none, }; +@UntilDestroy() @Component({ selector: 'asset-sg-editor-tab-administration', templateUrl: './asset-editor-tab-administration.component.html', @@ -46,6 +49,7 @@ export class AssetEditorTabAdministrationComponent implements OnInit { public _referenceDataVM$ = this._state.select('referenceDataVM'); public _assetEditDetail$ = this._state.select('assetEditDetail'); + private readonly dialogService = inject(MatDialog); private readonly filteredAssetEditDetail$ = this._state .select('assetEditDetail') @@ -60,8 +64,8 @@ export class AssetEditorTabAdministrationComponent implements OnInit { ); // eslint-disable-next-line @angular-eslint/no-output-rename - @Output('save') - public save$ = new EventEmitter(); + @Output() public saveAsset = new EventEmitter(); + @Output() public deleteAsset = new EventEmitter(); constructor() { this._state.set(initialTabAdministrationState); @@ -85,6 +89,18 @@ export class AssetEditorTabAdministrationComponent implements OnInit { } public save() { - this.save$.emit(); + this.saveAsset.emit(); + } + + public openConfirmDialog() { + const dialogRef = this.dialogService.open(ConfirmDialogComponent, { + data: { + text: 'confirmDelete', + }, + }); + dialogRef.componentInstance.confirmEvent.pipe(untilDestroyed(this)).subscribe(() => { + this.deleteAsset.emit(); + dialogRef.close(); + }); } } diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html index 7a02e7d7..317fac8e 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html @@ -28,7 +28,8 @@ @@ -50,5 +51,6 @@ asset-sg-icon-button class="absolute t-1.5 r-3" [attr.alt]="'edit.closeManageAsset' | translate" - > +> + + diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts index d20dbf05..2fd3c826 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts @@ -267,6 +267,11 @@ export class AssetEditorTabPageComponent { }); } + delete(): void { + const { general } = this.form.getRawValue(); + this._store.dispatch(actions.deleteAsset({ assetId: general.id })); + } + save(): void { if (this.form.invalid) { return; diff --git a/libs/asset-editor/src/lib/services/asset-editor.service.ts b/libs/asset-editor/src/lib/services/asset-editor.service.ts index 7b6ed8db..51308f9d 100644 --- a/libs/asset-editor/src/lib/services/asset-editor.service.ts +++ b/libs/asset-editor/src/lib/services/asset-editor.service.ts @@ -1,12 +1,12 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { ApiError, httpErrorResponseError } from '@asset-sg/client-shared'; -import { OE, ORD, decodeError, unknownError } from '@asset-sg/core'; +import { decodeError, OE, ORD, unknownError } from '@asset-sg/core'; import { Contact, PatchAsset, PatchContact } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; import * as E from 'fp-ts/Either'; import { flow } from 'fp-ts/function'; -import { concat, forkJoin, map, of, startWith, toArray } from 'rxjs'; +import { concat, forkJoin, map, Observable, of, startWith, toArray } from 'rxjs'; import { AssetEditorNewFile } from '../components/asset-editor-form-group'; import { AssetEditDetail } from '../models'; @@ -51,6 +51,10 @@ export class AssetEditorService { ); } + public deleteAsset(assetId: number): Observable { + return this.httpClient.delete(`/api/asset-edit/${assetId}`); + } + public deleteFiles(assetId: number, fileIds: number[]): ORD.ObservableRemoteData { return fileIds.length ? forkJoin( diff --git a/libs/asset-editor/src/lib/state/asset-editor.actions.ts b/libs/asset-editor/src/lib/state/asset-editor.actions.ts index 00d9eab2..a8206d65 100644 --- a/libs/asset-editor/src/lib/state/asset-editor.actions.ts +++ b/libs/asset-editor/src/lib/state/asset-editor.actions.ts @@ -24,6 +24,9 @@ export const updateAssetEditDetailResult = createAction( props>() ); +export const deleteAsset = createAction('[Asset Editor] Delete asset', props<{ assetId: number }>()); + +export const handleSuccessfulDeletion = createAction('[Asset Editor] Handle successful deletion'); export const editContact = createAction('[Asset Editor] Edit contact', props<{ contact: ContactEdit }>()); export const createContact = createAction('[Asset Editor] Create contact', props<{ contact: PatchContact }>()); diff --git a/libs/asset-editor/src/lib/state/asset-editor.effects.ts b/libs/asset-editor/src/lib/state/asset-editor.effects.ts index 63eb6ca2..dd246f33 100644 --- a/libs/asset-editor/src/lib/state/asset-editor.effects.ts +++ b/libs/asset-editor/src/lib/state/asset-editor.effects.ts @@ -1,14 +1,15 @@ -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Router } from '@angular/router'; -import { appSharedStateActions, filterNavigateToComponent } from '@asset-sg/client-shared'; +import { Alert, AlertType, appSharedStateActions, filterNavigateToComponent, showAlert } from '@asset-sg/client-shared'; import { DT, ORD, partitionEither } from '@asset-sg/core'; import * as RD from '@devexperts/remote-data-ts'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { TranslateService } from '@ngx-translate/core'; import { pipe } from 'fp-ts/function'; import * as O from 'fp-ts/Option'; import * as D from 'io-ts/Decoder'; -import { Observable, concatMap, map, partition, share, switchMap, tap } from 'rxjs'; +import { concatMap, map, Observable, partition, share, switchMap, tap } from 'rxjs'; import { AssetEditorPageComponent } from '../components/asset-editor-page'; import { AssetEditorService } from '../services/asset-editor.service'; @@ -21,6 +22,7 @@ export class AssetEditorEffects { private _actions$ = inject(Actions); private _assetEditorService = inject(AssetEditorService); private _router = inject(Router); + private readonly translateService = inject(TranslateService); validatedQueryParams = partitionEither( filterNavigateToComponent(this._actions$, AssetEditorPageComponent).pipe( @@ -81,6 +83,36 @@ export class AssetEditorEffects { ) ); + deleteAsset$ = createEffect(() => + this._actions$.pipe( + ofType(actions.deleteAsset), + switchMap(({ assetId }) => this._assetEditorService.deleteAsset(assetId)), + map(() => actions.handleSuccessfulDeletion()) + ) + ); + + displayAlertAfterSuccessfulDeletion$ = createEffect(() => + this._actions$.pipe( + ofType(actions.handleSuccessfulDeletion), + map(() => { + const alert: Alert = { + type: AlertType.Success, + text: this.translateService.instant('deleteSuccess'), + id: 'asset-deleted', + isPersistent: false, + }; + return showAlert({ alert }); + }) + ) + ); + + updateSearchAfterAssetChanged$ = createEffect(() => + this._actions$.pipe( + ofType(actions.handleSuccessfulDeletion, actions.updateAssetEditDetailResult), + map(() => appSharedStateActions.updateSearchAfterAssetEditedOrAdded({ assetId: undefined })) + ) + ); + createContact$ = createEffect(() => this._actions$.pipe( ofType(actions.createContact), diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index f0c7aa5f..2d886f9a 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; -import { ActivatedRoute, Params, Router } from '@angular/router'; -import { AppState, assetsPageMatcher, fromAppShared } from '@asset-sg/client-shared'; +import { Params, Router } from '@angular/router'; +import { appSharedStateActions, AppState, assetsPageMatcher, fromAppShared } from '@asset-sg/client-shared'; import { deepEqual, isDecodeError, isNotNull, ORD } from '@asset-sg/core'; import { AssetSearchQuery, AssetSearchResult, LV95, Polygon } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; @@ -29,7 +29,6 @@ export class AssetSearchEffects { private readonly store = inject(Store); private readonly actions$ = inject(Actions); private readonly router = inject(Router); - private readonly route = inject(ActivatedRoute); private readonly assetSearchService = inject(AssetSearchService); private readonly allStudyService = inject(AllStudyService); @@ -47,7 +46,12 @@ export class AssetSearchEffects { public loadSearch$ = createEffect(() => this.actions$.pipe( - ofType(actions.search, actions.resetSearch, actions.removePolygon), + ofType( + actions.search, + actions.resetSearch, + actions.removePolygon, + appSharedStateActions.updateSearchAfterAssetEditedOrAdded + ), withLatestFrom(this.store.select(selectAssetSearchQuery)), map(([_, query]) => actions.loadSearch({ query })) ) @@ -77,10 +81,10 @@ export class AssetSearchEffects { public toggleAssetDetail$ = createEffect(() => { return this.actions$.pipe( - ofType(actions.assetClicked), + ofType(actions.assetClicked, appSharedStateActions.updateSearchAfterAssetEditedOrAdded), withLatestFrom(this.store.select(selectCurrentAssetDetail)), switchMap(([{ assetId }, currentAssetDetail]) => - assetId !== currentAssetDetail?.assetId + assetId && assetId !== currentAssetDetail?.assetId ? this.assetSearchService .loadAssetDetailData(assetId) .pipe(map((assetDetail) => actions.updateAssetDetail({ assetDetail }))) diff --git a/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.html b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.html new file mode 100644 index 00000000..24e5058d --- /dev/null +++ b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.html @@ -0,0 +1,9 @@ +
+
+

{{ data.text | translate }}

+
+
+ + +
+
diff --git a/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.scss b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.scss new file mode 100644 index 00000000..ad57cefd --- /dev/null +++ b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.scss @@ -0,0 +1,6 @@ +.actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; +} diff --git a/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.ts b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 00000000..7952daad --- /dev/null +++ b/libs/client-shared/src/lib/components/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatOption } from '@angular/material/autocomplete'; +import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef } from '@angular/material/dialog'; +import { MatDivider } from '@angular/material/divider'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatSelect } from '@angular/material/select'; +import { TranslateModule } from '@ngx-translate/core'; +import { LetModule } from '@rx-angular/template/let'; +import { ButtonComponent } from '../button'; + +@Component({ + selector: 'asset-sg-disclaimer-dialog', + standalone: true, + imports: [ + CommonModule, + ButtonComponent, + FormsModule, + MatDialogActions, + MatDialogContent, + MatError, + MatFormField, + MatLabel, + MatOption, + MatSelect, + ReactiveFormsModule, + TranslateModule, + MatDivider, + LetModule, + ], + templateUrl: './confirm-dialog.component.html', + styleUrl: './confirm-dialog.component.scss', +}) +export class ConfirmDialogComponent { + @Output() public confirmEvent = new EventEmitter(); + + constructor( + private readonly dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + text: string; + } + ) {} + + public close() { + this.dialogRef.close(); + } + + public confirm() { + this.confirmEvent.emit(); + } +} diff --git a/libs/client-shared/src/lib/components/confirm-dialog/index.ts b/libs/client-shared/src/lib/components/confirm-dialog/index.ts new file mode 100644 index 00000000..f645b8dd --- /dev/null +++ b/libs/client-shared/src/lib/components/confirm-dialog/index.ts @@ -0,0 +1 @@ +export * from './confirm-dialog.component'; diff --git a/libs/client-shared/src/lib/components/index.ts b/libs/client-shared/src/lib/components/index.ts index 9b2bf182..570ec743 100644 --- a/libs/client-shared/src/lib/components/index.ts +++ b/libs/client-shared/src/lib/components/index.ts @@ -8,3 +8,4 @@ export * from './ol-zoom-controls'; export * from './value-item'; export * from './view-child-marker'; export * from './smart-translate.pipe'; +export * from './confirm-dialog'; diff --git a/libs/client-shared/src/lib/state/app-shared-state.actions.ts b/libs/client-shared/src/lib/state/app-shared-state.actions.ts index 10f9581d..262de333 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.actions.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.actions.ts @@ -41,3 +41,10 @@ export const logout = createAction('[App Shared State] Logout'); export const setLang = createAction('[App Shared State] Set Lang', props<{ lang: Lang }>()); export const toggleSearchFilter = createAction('[App Shared State] Toggle Search Filter'); + +export const updateSearchAfterAssetEditedOrAdded = createAction( + '[App Shared State] Handle Asset Changed', + props<{ + assetId?: number; + }>() +); diff --git a/libs/persistence/prisma/migrations/20241024140253_delete_asset_cascade/migration.sql b/libs/persistence/prisma/migrations/20241024140253_delete_asset_cascade/migration.sql new file mode 100644 index 00000000..6973dd4e --- /dev/null +++ b/libs/persistence/prisma/migrations/20241024140253_delete_asset_cascade/migration.sql @@ -0,0 +1,113 @@ +-- DropForeignKey +ALTER TABLE "asset_contact" DROP CONSTRAINT "asset_contact_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_format_composition" DROP CONSTRAINT "asset_format_composition_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_internal_project" DROP CONSTRAINT "asset_internal_project_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_kind_composition" DROP CONSTRAINT "asset_kind_composition_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_language" DROP CONSTRAINT "asset_language_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_publication" DROP CONSTRAINT "asset_publication_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_user_favourite" DROP CONSTRAINT "asset_user_favourite_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_user_favourite" DROP CONSTRAINT "asset_user_favourite_asset_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_x_asset_y" DROP CONSTRAINT "asset_x_asset_y_asset_x_id_fkey"; + +-- DropForeignKey +ALTER TABLE "asset_x_asset_y" DROP CONSTRAINT "asset_x_asset_y_asset_y_id_fkey"; + +-- DropForeignKey +ALTER TABLE "auto_cat" DROP CONSTRAINT "auto_cat_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "id" DROP CONSTRAINT "id_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "man_cat_label_ref" DROP CONSTRAINT "man_cat_label_ref_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "status_work" DROP CONSTRAINT "status_work_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "study_area" DROP CONSTRAINT "study_area_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "study_location" DROP CONSTRAINT "study_location_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "study_trace" DROP CONSTRAINT "study_trace_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "type_nat_rel" DROP CONSTRAINT "type_nat_rel_asset_id_fkey"; + +-- AddForeignKey +ALTER TABLE "id" ADD CONSTRAINT "id_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_x_asset_y" ADD CONSTRAINT "asset_x_asset_y_asset_x_id_fkey" FOREIGN KEY ("asset_x_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_x_asset_y" ADD CONSTRAINT "asset_x_asset_y_asset_y_id_fkey" FOREIGN KEY ("asset_y_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "man_cat_label_ref" ADD CONSTRAINT "man_cat_label_ref_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_format_composition" ADD CONSTRAINT "asset_format_composition_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_kind_composition" ADD CONSTRAINT "asset_kind_composition_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "status_work" ADD CONSTRAINT "status_work_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auto_cat" ADD CONSTRAINT "auto_cat_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "type_nat_rel" ADD CONSTRAINT "type_nat_rel_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_contact" ADD CONSTRAINT "asset_contact_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_publication" ADD CONSTRAINT "asset_publication_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_internal_project" ADD CONSTRAINT "asset_internal_project_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "study_area" ADD CONSTRAINT "study_area_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "study_location" ADD CONSTRAINT "study_location_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "study_trace" ADD CONSTRAINT "study_trace_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_language" ADD CONSTRAINT "asset_language_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_user_favourite" ADD CONSTRAINT "asset_user_favourite_asset_user_id_fkey" FOREIGN KEY ("asset_user_id") REFERENCES "asset_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "asset_user_favourite" ADD CONSTRAINT "asset_user_favourite_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "asset"("asset_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/libs/persistence/prisma/schema.prisma b/libs/persistence/prisma/schema.prisma index 8c77bd8b..04557e90 100644 --- a/libs/persistence/prisma/schema.prisma +++ b/libs/persistence/prisma/schema.prisma @@ -76,7 +76,7 @@ model Asset { model Id { idId Int @id @default(autoincrement()) @map("id_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) id String @map("id") description String @map("description") @@ -85,10 +85,10 @@ model Id { model AssetXAssetY { assetXId Int @map("asset_x_id") - assetX Asset @relation("sibling_x_asset", fields: [assetXId], references: [assetId]) + assetX Asset @relation("sibling_x_asset", fields: [assetXId], references: [assetId], onDelete: Cascade) assetYId Int @map("asset_y_id") - assetY Asset @relation("sibling_y_asset", fields: [assetYId], references: [assetId]) + assetY Asset @relation("sibling_y_asset", fields: [assetYId], references: [assetId], onDelete: Cascade) @@id([assetXId, assetYId]) @@map("asset_x_asset_y") @@ -120,7 +120,7 @@ model PublicUse { model AssetFile { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) fileId Int @map("file_id") file File @relation(fields: [fileId], references: [id], onDelete: Cascade) @@ -177,7 +177,7 @@ model AssetObjectInfo { model ManCatLabelRef { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) manCatLabelItemCode String @map("man_cat_label_item_code") manCatLabelItem ManCatLabelItem @relation(fields: [manCatLabelItemCode], references: [manCatLabelItemCode]) @@ -188,7 +188,7 @@ model ManCatLabelRef { model AssetFormatComposition { assetFormatCompositionId Int @id @default(autoincrement()) @map("asset_format_composition_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) assetFormatItemCode String @map("asset_format_item_code") assetFormatItem AssetFormatItem @relation(fields: [assetFormatItemCode], references: [assetFormatItemCode]) @@ -198,7 +198,7 @@ model AssetFormatComposition { model AssetKindComposition { assetKindCompositionId Int @id @default(autoincrement()) @map("asset_kind_composition_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) assetKindItemCode String @map("asset_kind_item_code") assetKindItem AssetKindItem @relation(fields: [assetKindItemCode], references: [assetKindItemCode]) @@ -208,7 +208,7 @@ model AssetKindComposition { model StatusWork { statusWorkId Int @id @default(autoincrement()) @map("status_work_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) statusWorkItemCode String @map("status_work_item_code") statusWorkItem StatusWorkItem @relation(fields: [statusWorkItemCode], references: [statusWorkItemCode]) statusWorkDate DateTime @map("status_work_date") @@ -220,7 +220,7 @@ model StatusWork { model AutoCat { autoCatId Int @id @default(autoincrement()) @map("auto_cat_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) autoCatLabelItemCode String @map("auto_cat_label_item_code") autoCatLabelItem AutoCatLabelItem @relation(fields: [autoCatLabelItemCode], references: [autoCatLabelItemCode]) autoCatLabelScore Int @map("auto_cat_label_score") @@ -231,7 +231,7 @@ model AutoCat { model TypeNatRel { typeNatRelId Int @id @default(autoincrement()) @map("type_nat_rel_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) natRelItemCode String @map("nat_rel_item_code") natRelItem NatRelItem @relation(fields: [natRelItemCode], references: [natRelItemCode]) @@ -259,7 +259,7 @@ model Contact { model AssetContact { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) contactId Int @map("contact_id") contact Contact @relation(fields: [contactId], references: [contactId]) role String @map("role") @@ -283,7 +283,7 @@ model Publication { model AssetPublication { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) publicationId Int @map("publication_id") publication Publication @relation(fields: [publicationId], references: [publicationId]) @@ -304,7 +304,7 @@ model InternalProject { model AssetInternalProject { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) internalProjectId Int @map("internal_project_id") internalProject InternalProject @relation(fields: [internalProjectId], references: [internalProjectId]) @@ -315,7 +315,7 @@ model AssetInternalProject { model StudyArea { studyAreaId Int @id @default(autoincrement()) @map("study_area_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) geomQualityItemCode String @map("geom_quality_item_code") geomQualityItem GeomQualityItem @relation(fields: [geomQualityItemCode], references: [geomQualityItemCode]) geom Unsupported("geometry")? @@ -327,7 +327,7 @@ model StudyArea { model StudyLocation { studyLocationId Int @id @default(autoincrement()) @map("study_location_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) geomQualityItemCode String @map("geom_quality_item_code") geomQualityItem GeomQualityItem @relation(fields: [geomQualityItemCode], references: [geomQualityItemCode]) geom Unsupported("geometry")? @@ -339,7 +339,7 @@ model StudyLocation { model StudyTrace { studyTraceId Int @id @default(autoincrement()) @map("study_trace_id") assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) geomQualityItemCode String @map("geom_quality_item_code") geomQualityItem GeomQualityItem @relation(fields: [geomQualityItemCode], references: [geomQualityItemCode]) geom Unsupported("geometry")? @@ -501,7 +501,7 @@ model LanguageItem { model AssetLanguage { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) languageItemCode String @map("language_item_code") languageItem LanguageItem @relation(fields: [languageItemCode], references: [languageItemCode]) @@ -649,10 +649,10 @@ model AssetUser { model AssetUserFavourite { assetUserId String @map("asset_user_id") @db.Uuid - AssetUser AssetUser @relation(fields: [assetUserId], references: [id]) + AssetUser AssetUser @relation(fields: [assetUserId], references: [id], onDelete: Cascade) assetId Int @map("asset_id") - Asset Asset @relation(fields: [assetId], references: [assetId]) + Asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) created_at DateTime @db.Timestamptz(6) @@ -690,7 +690,7 @@ enum Role { view AllStudy { assetId Int @map("asset_id") - asset Asset @relation(fields: [assetId], references: [assetId]) + asset Asset @relation(fields: [assetId], references: [assetId], onDelete: Cascade) id Int studyId String @unique @map("study_id") geom Unsupported("geometry")?