diff --git a/README.md b/README.md index 4cdac6f2..17768251 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ To do so, use the following commands. Be aware that you need to manually insert the `{DB_*}` values beforehand. ```bash cd development -docker compose exec db sh -c 'pg_dump --dbname=postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_DATABASE} --data-only --exclude-table asset_user -n public > /dump.sql' +docker compose exec db sh -c 'pg_dump --dbname=postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_DATABASE} --data-only --exclude-table asset_user _prisma_migrations -n public > /dump.sql' ``` > The export will output warnings related to circular foreign-key constraints. > These can be safely ignored. diff --git a/apps/server-asset-sg/src/app/app.service.ts b/apps/server-asset-sg/src/app/app.service.ts index 0da96d33..57f99aef 100644 --- a/apps/server-asset-sg/src/app/app.service.ts +++ b/apps/server-asset-sg/src/app/app.service.ts @@ -268,7 +268,11 @@ export class AppService { lastProcessedDate: true, assetKindItemCode: true, assetFormatItemCode: true, - languageItemCode: true, + assetLanguages: { + select: { + languageItem: true, + } + }, internalUse: { select: { isAvailable: true } }, publicUse: { select: { isAvailable: true } }, ids: { select: { id: true, description: true } }, diff --git a/apps/server-asset-sg/src/app/asset-edit/asset-edit.fake.ts b/apps/server-asset-sg/src/app/asset-edit/asset-edit.fake.ts index 4cff68c6..5d767931 100644 --- a/apps/server-asset-sg/src/app/asset-edit/asset-edit.fake.ts +++ b/apps/server-asset-sg/src/app/asset-edit/asset-edit.fake.ts @@ -52,7 +52,7 @@ export const fakeAssetPatch = (): PatchAsset => ({ internalUse: fakeAssetUsage(), publicUse: fakeAssetUsage(), isNatRel: false, - languageItemCode: fakeLanguageItemCode(), + assetLanguages: [], manCatLabelRefs: [], newStatusWorkItemCode: O.none, newStudies: [], @@ -83,7 +83,7 @@ export const fakeAssetEditDetail = (): AssetEditDetail => ({ internalUse: fakeAssetUsage(), publicUse: fakeAssetUsage(), isNatRel: false, - languageItemCode: fakeLanguageItemCode(), + assetLanguages: [], lastProcessedDate: new Date(), manCatLabelRefs: [], municipality: '', diff --git a/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts b/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts index 76e7c9ba..054a86b8 100644 --- a/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts +++ b/apps/server-asset-sg/src/app/jwt/jwt-middleware.ts @@ -10,6 +10,7 @@ import * as jwt from 'jsonwebtoken'; import { Jwt, JwtPayload } from 'jsonwebtoken'; import jwkToPem from 'jwk-to-pem'; +import { environment } from '../../environments/environment'; import { AuthenticatedRequest } from '../models/request'; import { PrismaService } from '../prisma/prisma.service'; @@ -116,9 +117,12 @@ export class JwtMiddleware implements NestMiddleware { } private getJwkTE(): TE.TaskEither { + const jwksPath = environment.production + ? '/.well-known/jwks.json' + : '/.well-known/openid-configuration/jwks'; return pipe( TE.tryCatch( - () => axios.get(`${process.env.OAUTH_ISSUER}/.well-known/jwks.json`), + () => axios.get(`${process.env.OAUTH_ISSUER}${jwksPath}`), reason => new Error(`${reason}`), ), TE.map(response => response.data.keys), diff --git a/apps/server-asset-sg/src/app/models/AssetDetailFromPostgres.ts b/apps/server-asset-sg/src/app/models/AssetDetailFromPostgres.ts index 42a09903..f0dfaef3 100644 --- a/apps/server-asset-sg/src/app/models/AssetDetailFromPostgres.ts +++ b/apps/server-asset-sg/src/app/models/AssetDetailFromPostgres.ts @@ -1,4 +1,5 @@ import { pipe } from 'fp-ts/function'; +import * as C from 'io-ts/Codec'; import * as D from 'io-ts/Decoder'; import { DT } from '@asset-sg/core'; @@ -23,13 +24,32 @@ export const AssetDetailFromPostgres = pipe( ), assetKindItemCode: D.string, assetFormatItemCode: D.string, - languageItemCode: D.string, ids: D.array( D.struct({ id: D.string, description: D.string, }), ), + assetLanguages: D.array( + D.struct({ + languageItem: D.struct({ + languageItemCode: C.string, + geolCode: C.string, + name: C.string, + nameDe: C.string, + nameFr: C.string, + nameRm: C.string, + nameIt: C.string, + nameEn: C.string, + description: C.string, + descriptionDe: C.string, + descriptionFr: C.string, + descriptionRm: C.string, + descriptionIt: C.string, + descriptionEn: C.string, + }), + }) + ), assetContacts: D.array( D.struct({ role: AssetContactRole, diff --git a/apps/server-asset-sg/src/app/models/asset-edit-detail.ts b/apps/server-asset-sg/src/app/models/asset-edit-detail.ts index 2ff33056..d0a0a9b7 100644 --- a/apps/server-asset-sg/src/app/models/asset-edit-detail.ts +++ b/apps/server-asset-sg/src/app/models/asset-edit-detail.ts @@ -2,7 +2,7 @@ import { pipe } from 'fp-ts/function'; import * as D from 'io-ts/Decoder'; import { DT } from '@asset-sg/core'; -import { AssetContactEdit, DateIdFromDate, LinkedAsset, StatusAssetUseCode } from '@asset-sg/shared'; +import { AssetContactEdit, AssetLanguageEdit, DateIdFromDate, LinkedAsset, StatusAssetUseCode } from '@asset-sg/shared'; import { PostgresAllStudies } from '../postgres-studies/postgres-studies'; @@ -27,7 +27,6 @@ export const AssetEditDetailFromPostgres = pipe( }), assetKindItemCode: D.string, assetFormatItemCode: D.string, - languageItemCode: D.string, isNatRel: D.boolean, sgsId: D.nullable(D.number), geolDataInfo: D.nullable(D.string), @@ -41,6 +40,7 @@ export const AssetEditDetailFromPostgres = pipe( description: D.string, }), ), + assetLanguages: D.array(AssetLanguageEdit), assetContacts: D.array(AssetContactEdit), manCatLabelRefs: D.array( pipe( diff --git a/apps/server-asset-sg/src/app/prisma/migrations/20240418060636_multi_language_assets/migration.sql b/apps/server-asset-sg/src/app/prisma/migrations/20240418060636_multi_language_assets/migration.sql new file mode 100644 index 00000000..a572445f --- /dev/null +++ b/apps/server-asset-sg/src/app/prisma/migrations/20240418060636_multi_language_assets/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `language_item_code` on the `asset` table. All the data in the column will be lost. + +*/ + +-- CreateTable +CREATE TABLE "public"."asset_language" ( + "asset_id" INTEGER NOT NULL, + "language_item_code" TEXT NOT NULL, + + CONSTRAINT "asset_language_pkey" PRIMARY KEY ("asset_id","language_item_code") +); + +-- AddForeignKey +ALTER TABLE "public"."asset_language" ADD CONSTRAINT "asset_language_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "public"."asset"("asset_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."asset_language" ADD CONSTRAINT "asset_language_language_item_code_fkey" FOREIGN KEY ("language_item_code") REFERENCES "public"."language_item"("language_item_code") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- MigrateLanguages +INSERT INTO "public"."asset_language" ("asset_id", "language_item_code") +SELECT a.asset_id, a.language_item_code FROM "public"."asset" a; + +-- DropForeignKey +ALTER TABLE "public"."asset" DROP CONSTRAINT "asset_language_item_code_fkey"; + +-- AlterTable +ALTER TABLE "public"."asset" DROP COLUMN "language_item_code"; diff --git a/apps/server-asset-sg/src/app/prisma/schema.prisma b/apps/server-asset-sg/src/app/prisma/schema.prisma index d9060338..69953835 100644 --- a/apps/server-asset-sg/src/app/prisma/schema.prisma +++ b/apps/server-asset-sg/src/app/prisma/schema.prisma @@ -135,8 +135,6 @@ model Asset { assetKindItemCode String @map("asset_kind_item_code") assetKindItem AssetKindItem @relation(fields: [assetKindItemCode], references: [assetKindItemCode]) createDate DateTime @map("create_date") - languageItemCode String @map("language_item_code") - languageItem LanguageItem @relation(fields: [languageItemCode], references: [languageItemCode]) assetFormatItemCode String @map("asset_format_item_code") assetFormatItem AssetFormatItem @relation(fields: [assetFormatItemCode], references: [assetFormatItemCode]) authorBiblio String? @map("author_biblio_id") @@ -155,6 +153,7 @@ model Asset { assetFormatCompositions AssetFormatComposition[] assetKindCompositions AssetKindComposition[] assetPublications AssetPublication[] + assetLanguages AssetLanguage[] autoCats AutoCat[] ids Id[] legalDocs LegalDoc[] @@ -630,12 +629,23 @@ model LanguageItem { descriptionIt String @map("description_it") descriptionEn String @map("description_en") - assets Asset[] + assets AssetLanguage[] @@map("language_item") @@schema("public") } +model AssetLanguage { + assetId Int @map("asset_id") + asset Asset @relation(fields: [assetId], references: [assetId]) + languageItemCode String @map("language_item_code") + languageItem LanguageItem @relation(fields: [languageItemCode], references: [languageItemCode]) + + @@id([assetId, languageItemCode]) + @@map("asset_language") + @@schema("public") +} + model ManCatLabelItem { manCatLabelItemCode String @id @map("man_cat_label_item_code") geolCode String @map("geol_code") diff --git a/apps/server-asset-sg/src/app/repos/asset.repo.spec.ts b/apps/server-asset-sg/src/app/repos/asset.repo.spec.ts index 89ec7ff3..6f494774 100644 --- a/apps/server-asset-sg/src/app/repos/asset.repo.spec.ts +++ b/apps/server-asset-sg/src/app/repos/asset.repo.spec.ts @@ -32,10 +32,10 @@ describe(AssetRepo, () => { const user = fakeUser(); const patch = fakeAssetPatch(); const expected = await repo.create({ patch, user }); - + // When const actual = await repo.find(expected.assetId); - + // Then expect(actual).not.toBeNull(); expect(actual).toEqual(expected); @@ -122,7 +122,6 @@ describe(AssetRepo, () => { expect(record.internalUse).toEqual(patch.internalUse); expect(record.assetKindItemCode).toEqual(patch.assetKindItemCode); expect(record.assetFormatItemCode).toEqual(patch.assetFormatItemCode); - expect(record.languageItemCode).toEqual(patch.languageItemCode); expect(record.isNatRel).toEqual(patch.isNatRel); expect(record.sgsId).toBeNull(); expect(record.geolDataInfo).toEqual(null); @@ -131,6 +130,7 @@ describe(AssetRepo, () => { expect(record.municipality).toBeNull(); expect(record.ids).toEqual(patch.ids); expect(record.assetContacts).toEqual(patch.assetContacts); + expect(record.assetLanguages).toEqual(patch.assetLanguages); expect(record.manCatLabelRefs).toEqual(patch.manCatLabelRefs); expect(record.assetFormatCompositions).toEqual([]); expect(record.typeNatRels).toEqual(patch.typeNatRels); @@ -179,7 +179,6 @@ describe(AssetRepo, () => { expect(updated.internalUse).toEqual(patch.internalUse); expect(updated.assetKindItemCode).toEqual(patch.assetKindItemCode); expect(updated.assetFormatItemCode).toEqual(patch.assetFormatItemCode); - expect(updated.languageItemCode).toEqual(patch.languageItemCode); expect(updated.isNatRel).toEqual(patch.isNatRel); expect(updated.sgsId).toBeNull(); expect(updated.geolDataInfo).toEqual(null); @@ -188,6 +187,7 @@ describe(AssetRepo, () => { expect(updated.municipality).toBeNull(); expect(updated.ids).toEqual(patch.ids); expect(updated.assetContacts).toEqual(patch.assetContacts); + expect(updated.assetLanguages).toEqual(patch.assetLanguages); expect(updated.manCatLabelRefs).toEqual(patch.manCatLabelRefs); expect(updated.assetFormatCompositions).toEqual([]); expect(updated.typeNatRels).toEqual(patch.typeNatRels); diff --git a/apps/server-asset-sg/src/app/repos/asset.repo.ts b/apps/server-asset-sg/src/app/repos/asset.repo.ts index 80e11cad..2747d4be 100644 --- a/apps/server-asset-sg/src/app/repos/asset.repo.ts +++ b/apps/server-asset-sg/src/app/repos/asset.repo.ts @@ -57,7 +57,6 @@ export class AssetRepo implements Repo { receiptDate: DateIdFromDate.encode(data.patch.receiptDate), assetKindItem: { connect: { assetKindItemCode: data.patch.assetKindItemCode } }, assetFormatItem: { connect: { assetFormatItemCode: data.patch.assetFormatItemCode } }, - languageItem: { connect: { languageItemCode: data.patch.languageItemCode } }, isExtract: false, isNatRel: data.patch.isNatRel, lastProcessedDate: new Date(), @@ -73,6 +72,9 @@ export class AssetRepo implements Repo { assetContacts: { createMany: { data: data.patch.assetContacts, skipDuplicates: true }, }, + assetLanguages: { + createMany: { data: data.patch.assetLanguages, skipDuplicates: true }, + }, ids: { createMany: { data: data.patch.ids.map(({ id, description }) => ({ id, description })), @@ -122,7 +124,6 @@ export class AssetRepo implements Repo { receiptDate: DateIdFromDate.encode(data.patch.receiptDate), assetKindItemCode: data.patch.assetKindItemCode, assetFormatItemCode: data.patch.assetFormatItemCode, - languageItemCode: data.patch.languageItemCode, isNatRel: data.patch.isNatRel, assetMainId: O.toUndefined(data.patch.assetMainId), lastProcessedDate: new Date(), @@ -140,6 +141,10 @@ export class AssetRepo implements Repo { deleteMany: {}, createMany: { data: data.patch.assetContacts, skipDuplicates: true }, }, + assetLanguages: { + deleteMany: {}, + createMany: { data: data.patch.assetLanguages, skipDuplicates: true }, + }, ids: { deleteMany: { idId: { @@ -245,6 +250,11 @@ export class AssetRepo implements Repo { 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 }, @@ -322,7 +332,6 @@ const selectPrismaAsset = selectOnAsset({ processor: true, assetKindItemCode: true, assetFormatItemCode: true, - languageItemCode: true, internalUse: true, publicUse: true, isNatRel: true, @@ -333,6 +342,7 @@ const selectPrismaAsset = selectOnAsset({ municipality: true, ids: true, assetContacts: { select: { role: true, contactId: true } }, + assetLanguages: { select: { languageItemCode: true } }, manCatLabelRefs: { select: { manCatLabelItemCode: true } }, assetFormatCompositions: { select: { assetFormatItemCode: true } }, typeNatRels: { select: { natRelItemCode: true } }, diff --git a/apps/server-asset-sg/src/app/search/asset-search-service.spec.ts b/apps/server-asset-sg/src/app/search/asset-search-service.spec.ts index e3665de6..5bbc7896 100644 --- a/apps/server-asset-sg/src/app/search/asset-search-service.spec.ts +++ b/apps/server-asset-sg/src/app/search/asset-search-service.spec.ts @@ -67,7 +67,7 @@ describe(AssetSearchService, () => { expect(hit.sgsId).toEqual(asset.sgsId); expect(hit.createDate).toEqual(asset.createDate); expect(hit.assetKindItemCode).toEqual(asset.assetKindItemCode); - expect(hit.languageItemCode).toEqual(asset.languageItemCode); + expect(hit.languageItemCodes).toEqual(asset.assetLanguages.map((it) => it.languageItemCode)); expect(hit.usageCode).toEqual(makeUsageCode(asset.publicUse.isAvailable, asset.internalUse.isAvailable)) expect(hit.authorIds).toEqual([]); expect(hit.contactNames).toEqual([]); @@ -197,14 +197,14 @@ describe(AssetSearchService, () => { assertSingleResult(result, asset); }) - it('finds assets by languageItemCode', async () => { + it('finds assets by languageItemCodes', async () => { // Given const code1 = languageItems[0].languageItemCode; const code2 = languageItems[1].languageItemCode; const code3 = languageItems[2].languageItemCode; - const asset = await create({ patch: { ...fakeAssetPatch(), languageItemCode: code1 }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), languageItemCode: code2 }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), languageItemCode: code3 }, user: fakeUser() }); + const asset = await create({ patch: { ...fakeAssetPatch(), assetLanguages: [{ languageItemCode: code1 }] }, user: fakeUser() }); + await create({ patch: { ...fakeAssetPatch(), assetLanguages: [{ languageItemCode: code2 }] }, user: fakeUser() }); + await create({ patch: { ...fakeAssetPatch(), assetLanguages: [{ languageItemCode: code3 }] }, user: fakeUser() }); // When const result = await service.search({ languageItemCodes: [code1] }); @@ -301,10 +301,10 @@ describe(AssetSearchService, () => { value: asset.assetKindItemCode, count: 1, }]); - expect(stats.languageItemCodes).toEqual([{ - value: asset.languageItemCode, + expect(stats.languageItemCodes).toEqual(asset.assetLanguages.map((it) => ({ + value: it.languageItemCode, count: 1, - },]); + }))); expect(stats.usageCodes).toEqual([{ value: makeUsageCode(asset.publicUse.isAvailable, asset.internalUse.isAvailable), count: 1, @@ -358,19 +358,22 @@ describe(AssetSearchService, () => { const makeBucket = ( expectedAssets: AssetEditDetail[], - extract: (asset: AssetEditDetail) => string | number, + extract: (asset: AssetEditDetail) => string | number | string[] | number[], ): Bucket[] => { return expectedAssets.reduce((buckets, asset) => { - const key = extract(asset); - const bucket = buckets.find((it) => it.key === key); - if (bucket == null) { - buckets.push({ key, count: 1 }); - } else { - bucket.count += 1; + const keyOrKeys = extract(asset); + const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; + for (const key of keys) { + const bucket = buckets.find((it) => it.key === key); + if (bucket == null) { + buckets.push({ key, count: 1 }); + } else { + bucket.count += 1; + } } return buckets; }, [] as Bucket[]); - }; + } const compareBuckets = (a: Bucket, b: Bucket): number => a.key.toString().localeCompare(b.key.toString()); @@ -457,7 +460,9 @@ describe(AssetSearchService, () => { expect(actualAsset.createDate).toEqual(expectedAsset.createDate); expect(actualAsset.assetFormatItemCode).toEqual(expectedAsset.assetFormatItemCode); expect(actualAsset.assetKindItemCode).toEqual(expectedAsset.assetKindItemCode); - expect(actualAsset.languageItemCode).toEqual(expectedAsset.languageItemCode); + expect(actualAsset.languages).toEqual( + expectedAsset.assetLanguages.map((it) => ({ code: it.languageItemCode })), + ); expect(actualAsset.contacts).toEqual([]); expect(actualAsset.studies).toEqual([]); expect(actualAsset.manCatLabelItemCodes).toEqual([]); @@ -481,7 +486,9 @@ describe(AssetSearchService, () => { expect(aggs.buckets.assetKindItemCodes.sort(compareBuckets)) .toEqual(expectedAssetKindItemCodes.sort(compareBuckets)); - const expectedLanguageItemCodes = makeBucket(expectedAssets, (asset) => asset.languageItemCode); + const expectedLanguageItemCodes = makeBucket(expectedAssets, (asset) => ( + asset.assetLanguages.map((it) => it.languageItemCode) + )); expect(aggs.buckets.languageItemCodes.sort(compareBuckets)) .toEqual(expectedLanguageItemCodes.sort(compareBuckets)); diff --git a/apps/server-asset-sg/src/app/search/asset-search-service.ts b/apps/server-asset-sg/src/app/search/asset-search-service.ts index 8b108880..6ec62649 100644 --- a/apps/server-asset-sg/src/app/search/asset-search-service.ts +++ b/apps/server-asset-sg/src/app/search/asset-search-service.ts @@ -390,10 +390,16 @@ export class AssetSearchService { }, { query_string: { - query: `*${query}*`, + query: `*${escapeElasticQuery(query)}*`, fields: scope, }, }, + { + query_string: { + query: query, + fields: scope, + } + } ], filter: filters, }, @@ -403,7 +409,7 @@ export class AssetSearchService { minCreateDate: { min: { field: 'createDate' } }, maxCreateDate: { max: { field: 'createDate' } }, assetKindItemCodes: { terms: { field: 'assetKindItemCode' } }, - languageItemCodes: { terms: { field: 'languageItemCode' } }, + languageItemCodes: { terms: { field: 'languageItemCodes' } }, usageCodes: { terms: { field: 'usageCode' } }, manCatLabelItemCodes: { terms: { field: 'manCatLabelItemCodes' } }, }, @@ -502,7 +508,7 @@ export class AssetSearchService { createDate: true, assetKindItemCode: true, assetFormatItemCode: true, - languageItemCode: true, + assetLanguages: true, internalUse: { select: { isAvailable: true, @@ -548,7 +554,7 @@ export class AssetSearchService { createDate: dateIdFromDate(entity.createDate), assetFormatItemCode: entity.assetFormatItemCode, assetKindItemCode: entity.assetKindItemCode, - languageItemCode: entity.languageItemCode, + languages: entity.assetLanguages.map((it) => ({ code: it.languageItemCode })), contacts: entity.assetContacts.map((contact) => ({ id: contact.contactId, role: contact.role, @@ -589,7 +595,7 @@ export class AssetSearchService { sgsId: asset.sgsId, createDate: asset.createDate, assetKindItemCode: asset.assetKindItemCode, - languageItemCode: asset.languageItemCode, + languageItemCodes: asset.assetLanguages.map((it) => it.languageItemCode), usageCode: makeUsageCode(asset.publicUse.isAvailable, asset.internalUse.isAvailable), authorIds: asset.assetContacts.filter((it) => it.role === 'author').map((it) => it.contactId), contactNames: contacts.map((it) => it.name), @@ -661,7 +667,7 @@ const mapQueryToElasticDsl = (query: AssetSearchQuery): QueryDslQueryContainer = filters.push(makeArrayFilter('usageCode', query.usageCodes)); } if (query.languageItemCodes != null) { - filters.push(makeArrayFilter('languageItemCode', query.languageItemCodes)); + filters.push(makeArrayFilter('languageItemCodes', query.languageItemCodes)); } if (query.geomCodes != null) { filters.push(makeArrayFilterOrNone('geometryCodes', query.geomCodes)); diff --git a/apps/server-asset-sg/src/app/search/search-asset.ts b/apps/server-asset-sg/src/app/search/search-asset.ts index 1f30a3a5..c73d9da2 100644 --- a/apps/server-asset-sg/src/app/search/search-asset.ts +++ b/apps/server-asset-sg/src/app/search/search-asset.ts @@ -30,12 +30,21 @@ const makeSearchAssets = ( return pipe( assetQueryResults, NEA.map((a): SearchAsset => { - const { createDate, manCatLabelRefs, internalUse, publicUse, assetContacts, ...rest } = a; + const { + createDate, + manCatLabelRefs, + internalUse, + publicUse, + assetLanguages, + assetContacts, + ...rest + } = a; return { ...rest, createDate: dateIdFromDate(createDate), manCatLabelItemCodes: manCatLabelRefs.map(m => m.manCatLabelItemCode), usageCode: makeUsageCode(publicUse.isAvailable, internalUse.isAvailable), + languages: assetLanguages.map(a => ({ code: a.languageItemCode })), contacts: assetContacts.map(c => ({ role: c.role, id: c.contactId })), score: 1, studies: pipe( @@ -86,7 +95,8 @@ const makeSearchAssetResultNonEmpty = (assets: NEA.NonEmptyArray) = languageItemCodes: makeBuckets( pipe( assets, - A.map(a => a.languageItemCode), + A.map(a => a.languages.map((l) => l.code)), + A.flatten, ), ), usageCodes: makeBuckets( @@ -152,10 +162,10 @@ export const searchAssetQuery = makeSearchAssetQuery({ createDate: true, assetKindItemCode: true, assetFormatItemCode: true, - languageItemCode: true, internalUse: { select: { isAvailable: true } }, publicUse: { select: { isAvailable: true } }, manCatLabelRefs: { select: { manCatLabelItemCode: true } }, + assetLanguages: { select: { languageItemCode: true } }, assetContacts: { select: { role: true, contactId: true } }, }, }); diff --git a/development/docker-compose.yaml b/development/docker-compose.yaml index 149e0877..05478017 100644 --- a/development/docker-compose.yaml +++ b/development/docker-compose.yaml @@ -119,24 +119,25 @@ services: environment: - ServerOptions__HostName=smtp4dev - oidc-server: + oidc: container_name: swissgeol-assets-oidc - image: soluto/oidc-server-mock + image: ghcr.io/soluto/oidc-server-mock restart: unless-stopped ports: - - "4011:80" + - "4011:8080" environment: CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json USERS_CONFIGURATION_PATH: /tmp/config/users-config.json - IDENTITY_RESOURCES_INLINE: | - [ - { - "Name": "local_groups_scope", - "ClaimTypes": [ - "local_groups_claim" - ] - } - ] + API_SCOPES_INLINE: | + [ + { + "Name": "cognito", + "UserClaims": [ + "cognito:groups", + "username" + ] + } + ] SERVER_OPTIONS_INLINE: | { "IssuerUri": "http://localhost:4011", @@ -147,8 +148,13 @@ services: "Authentication": { "CookieSameSiteMode": "Lax", "CheckSessionCookieSameSiteMode": "Lax" + }, + "KeyManagement": { + "Enabled": true, + "KeyPath": "/tmp/data/keys" } } volumes: - ./init/oidc/oidc-mock-clients.json:/tmp/config/clients-config.json:ro - - ./init/oidc/oidc-mock-users.json:/tmp/config/users-config.json:ro + - ./init/oidc/oidc-mock-users.json:/tmp/config/users-config.json:ro + - ./volumes/oidc/keys:/tmp/data/keys diff --git a/development/init/elasticsearch/mappings/swissgeol_asset_asset.json b/development/init/elasticsearch/mappings/swissgeol_asset_asset.json index 71467279..02a70f6c 100644 --- a/development/init/elasticsearch/mappings/swissgeol_asset_asset.json +++ b/development/init/elasticsearch/mappings/swissgeol_asset_asset.json @@ -24,7 +24,7 @@ "createDateId": { "type": "integer" }, - "languageItemCode": { + "languageItemCodes": { "type": "keyword" }, "manCatLabelItemCodes": { diff --git a/development/init/oidc/oidc-mock-clients.json b/development/init/oidc/oidc-mock-clients.json index 1d4a1b3d..209a9260 100644 --- a/development/init/oidc/oidc-mock-clients.json +++ b/development/init/oidc/oidc-mock-clients.json @@ -1,15 +1,11 @@ [{ - "ClientId": "assets-client", - "Description": "Client for Authorization Code flow with PKCE", + "ClientId": "assets", + "Description": "swisstopo assets", "RequireClientSecret": false, "AlwaysIncludeUserClaimsInIdToken": true, "AllowedGrantTypes": [ "authorization_code" ], - "AllowedResponseTypes": [ - "code", - "id_token" - ], "AllowAccessTokensViaBrowser": true, "RedirectUris": [ "http://localhost:4200" @@ -20,10 +16,10 @@ "AllowedScopes": [ "openid", "profile", - "local_groups_scope" + "email", + "cognito" ], "AccessTokenType": "JWT", "IdentityTokenLifetime": 3600, "AccessTokenLifetime": 3600 -} -] +}] diff --git a/development/init/oidc/oidc-mock-users.json b/development/init/oidc/oidc-mock-users.json index c2919e49..6ec00608 100644 --- a/development/init/oidc/oidc-mock-users.json +++ b/development/init/oidc/oidc-mock-users.json @@ -1,17 +1,17 @@ [ { - "SubjectId":"10f95aa3-fb95-41eb-b754-5f729a092e30", - "Username":"admin@swissgeol.assets", - "Password":"swissgeol_assets", + "SubjectId":"379a20e6-6a5d-4390-93ca-d408613e854d", + "Username":"admin", + "Password":"admin", "Claims": [ { "Type": "name", - "Value": "Admin User", + "Value": "Admin", "ValueType": "string" }, { "Type": "family_name", - "Value": "User", + "Value": "Admin", "ValueType": "string" }, { @@ -21,7 +21,7 @@ }, { "Type": "email", - "Value": "admin.user@local.dev", + "Value": "admin@assets.swissgeol.ch", "ValueType": "string" }, { @@ -30,35 +30,40 @@ "ValueType": "boolean" }, { - "Type": "local_groups_claim", - "Value": "[\"boreholes_dev_group\"]", + "Type": "cognito:groups", + "Value": "[\"assets.swissgeol\"]", "ValueType": "json" + }, + { + "Type": "username", + "Value": "1_admin@assets.swissgeol.ch", + "ValueType": "string" } ] }, { - "SubjectId":"sub_editor", - "Username":"editor", - "Password":"swissforages", + "SubjectId":"e06ad465-3adc-4ad7-bee5-ff0605a4b928", + "Username":"viewer", + "Password":"viewer", "Claims": [ { "Type": "name", - "Value": "Editor User", + "Value": "Viewer", "ValueType": "string" }, { "Type": "family_name", - "Value": "User", + "Value": "Viewer", "ValueType": "string" }, { "Type": "given_name", - "Value": "Editor", + "Value": "Viewer", "ValueType": "string" }, { "Type": "email", - "Value": "editor.user@local.dev", + "Value": "viewer@assets.swissgeol.ch", "ValueType": "string" }, { @@ -67,9 +72,14 @@ "ValueType": "boolean" }, { - "Type": "local_groups_claim", - "Value": "[\"boreholes_dev_group\"]", + "Type": "cognito:groups", + "Value": "[\"assets.swissgeol\"]", "ValueType": "json" + }, + { + "Type": "username", + "Value": "2_viewer@assets.swissgeol.ch", + "ValueType": "string" } ] } diff --git a/libs/asset-editor/src/lib/components/asset-editor-form-group.ts b/libs/asset-editor/src/lib/components/asset-editor-form-group.ts index 4b499cda..94a9a74d 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-form-group.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-form-group.ts @@ -3,7 +3,7 @@ import { AbstractControl, FormBuilder, FormControl, ValidatorFn, Validators } fr import { AssetContactEdit, - AssetFile, + AssetFile, AssetLanguageEdit, DateId, LinkedAsset, StatusAssetUseCode, @@ -27,7 +27,7 @@ const makeAssetEditorGeneralFormGroup = (formBuilder: FormBuilder) => }), assetKindItemCode: new FormControl('', { nonNullable: true, validators: Validators.required }), assetFormatItemCode: new FormControl('', { nonNullable: true, validators: Validators.required }), - languageItemCode: new FormControl('', { nonNullable: true, validators: Validators.required }), + assetLanguages: new FormControl([], { nonNullable: true }), manCatLabelRefs: new FormControl(['other'], { nonNullable: true }), ids: new FormControl([], { nonNullable: true }), assetFiles: new FormControl<(AssetFile & { willBeDeleted: boolean })[]>([], { nonNullable: true }), diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html index 82d52b71..6d8004a1 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html @@ -66,11 +66,16 @@ edit.tabs.general.language - + + [value]="{ languageItemCode: language.code }" + > {{ language | valueItemName }} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts index e5124606..6b86d16d 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts @@ -17,6 +17,7 @@ import { } from 'rxjs'; import { fromAppShared } from '@asset-sg/client-shared'; +import { eqAssetLanguageEdit } from '@asset-sg/shared'; import { eqIdVM } from '../../models'; import { AssetEditorFormGroup, AssetEditorGeneralFormGroup } from '../asset-editor-form-group'; @@ -259,4 +260,6 @@ export class AssetEditorTabGeneralComponent implements OnInit { ); this._form.markAsDirty(); } + + public eqAssetLanguageEdit = eqAssetLanguageEdit } 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 981c8e0b..cd71e49f 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 @@ -100,7 +100,7 @@ export class AssetEditorTabPageComponent { receiptDate: asset.receiptDate, assetKindItemCode: asset.assetKindItemCode, assetFormatItemCode: asset.assetFormatItemCode, - languageItemCode: asset.languageItemCode, + assetLanguages: asset.assetLanguages, manCatLabelRefs: asset.manCatLabelRefs, ids: asset.ids, filesToDelete: [], @@ -263,10 +263,10 @@ export class AssetEditorTabPageComponent { }, assetKindItemCode: this._form.getRawValue().general.assetKindItemCode, assetFormatItemCode: this._form.getRawValue().general.assetFormatItemCode, - languageItemCode: this._form.getRawValue().general.languageItemCode, isNatRel: this._form.getRawValue().usage.isNatRel, typeNatRels: this._form.getRawValue().usage.natRelTypeItemCodes, manCatLabelRefs: this._form.getRawValue().general.manCatLabelRefs, + assetLanguages: this._form.getRawValue().general.assetLanguages, assetContacts: this._form.getRawValue().contacts.assetContacts, ids: this._form.getRawValue().general.ids, studies: pipe( diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html index 44c59734..4e1be076 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html @@ -115,7 +115,13 @@ - {{ rdAssetDetail.value.languageItem | valueItemName }} + +
    +
  • {{ language | valueItemName }}
  • +
+ diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.scss b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.scss index 8cf22c08..b0e75080 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.scss +++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.scss @@ -153,3 +153,14 @@ li.link { margin-left: 0.5rem; } } + +// A list of languages to which an asset is mapped. +ul.languages { + display: flex; + list-style: none; + + // Insert commas between every two languages. + & > li:not(:last-child)::after { + content: ',\00a0'; + } +} diff --git a/libs/asset-viewer/src/lib/models/search-asset-result-client.ts b/libs/asset-viewer/src/lib/models/search-asset-result-client.ts index 7da9ff3d..cde741f1 100644 --- a/libs/asset-viewer/src/lib/models/search-asset-result-client.ts +++ b/libs/asset-viewer/src/lib/models/search-asset-result-client.ts @@ -14,7 +14,9 @@ export const SearchAssetClient = D.struct({ createDate: DateId, assetKindItemCode: D.string, assetFormatItemCode: D.string, - languageItemCode: D.string, + languages: D.array(D.struct({ + code: D.string, + })), manCatLabelItemCodes: D.array(D.string), usageCode: UsageCode, score: D.number, diff --git a/libs/asset-viewer/src/lib/state/asset-viewer.selectors.ts b/libs/asset-viewer/src/lib/state/asset-viewer.selectors.ts index 85c48bc7..3df20a9c 100644 --- a/libs/asset-viewer/src/lib/state/asset-viewer.selectors.ts +++ b/libs/asset-viewer/src/lib/state/asset-viewer.selectors.ts @@ -272,7 +272,7 @@ export type RDStudiesVM = RD.RemoteData { const { assetFormatItemCode, - languageItemCode, + assetLanguages, assetKindItemCode, assetContacts, manCatLabelRefs, @@ -290,7 +290,7 @@ const makeAssetDetailVM = (referenceData: ReferenceData, assetDetail: AssetDetai ...rest, assetKindItem: referenceData.assetKindItems[assetKindItemCode], assetFormatItem: referenceData.assetFormatItems[assetFormatItemCode], - languageItem: referenceData.languageItems[languageItemCode], + languages: assetLanguages.map(({ languageItem: { languageItemCode: code, ...restL } }) => ({ code, ...restL })), contacts: assetContacts.map(contact => makeAssetDetailContactVM(referenceData, contact)), manCatLabels: manCatLabelRefs.map(manCatLabelItemCode => referenceData.manCatLabelItems[manCatLabelItemCode]), assetFormatCompositions: assetFormatCompositions.map( @@ -403,7 +403,7 @@ const matchFromOption = (a: O.Option, predicate: (b: T) => boolean): boole const matchLanguageItemCode = (refinement: BaseClientAssetSearchRefinement, asset: SearchAssetVM): boolean => pipe( refinement.languageItemCodes, - O.map(cs => cs.some(languageCode => languageCode === asset.languageItem.code)), + O.map(cs => cs.some(languageCode => undefined !== asset.languageItems.find(({ code }) => code === languageCode))), O.getOrElse(() => true), ); @@ -498,13 +498,13 @@ const doesAssetMatchRefinement = ); const _makeSearchAssetVM = (referenceData: ReferenceData) => (asset: SearchAssetClient) => { - const { assetKindItemCode, assetFormatItemCode, contacts, languageItemCode, ...rest } = asset; + const { assetKindItemCode, assetFormatItemCode, contacts, languages, ...rest } = asset; return { ...rest, assetFormatItem: referenceData.assetFormatItems[assetFormatItemCode], assetKindItem: referenceData.assetKindItems[assetKindItemCode], manCatLabelItems: asset.manCatLabelItemCodes.map(code => referenceData.manCatLabelItems[code]), - languageItem: referenceData.languageItems[languageItemCode], + languageItems: languages.map(({ code }) => referenceData.languageItems[code]), authors: contacts .filter(c => c.role === 'author') .map(c => ({ role: c.role, contact: referenceData.contacts[c.id.toString()] })), diff --git a/libs/shared/src/lib/models/SearchAssetResult.ts b/libs/shared/src/lib/models/SearchAssetResult.ts index 9638b44b..fd56390c 100644 --- a/libs/shared/src/lib/models/SearchAssetResult.ts +++ b/libs/shared/src/lib/models/SearchAssetResult.ts @@ -13,10 +13,10 @@ export const SearchAsset = C.struct({ createDate: DateId, assetKindItemCode: C.string, assetFormatItemCode: C.string, - languageItemCode: C.string, manCatLabelItemCodes: C.array(C.string), score: C.number, studies: StudyDTOs, + languages: C.array(C.struct({ code: C.string })), contacts: C.array(C.struct({ role: C.string, id: C.number })), usageCode: UsageCode, }); diff --git a/libs/shared/src/lib/models/asset-detail.ts b/libs/shared/src/lib/models/asset-detail.ts index b03ba7da..ecd351a2 100644 --- a/libs/shared/src/lib/models/asset-detail.ts +++ b/libs/shared/src/lib/models/asset-detail.ts @@ -40,13 +40,32 @@ export const BaseAssetDetail = { usageCode: UsageCode, assetKindItemCode: C.string, assetFormatItemCode: C.string, - languageItemCode: C.string, ids: C.array( C.struct({ id: C.string, description: C.string, }), ), + assetLanguages: C.array( + C.struct({ + languageItem: C.struct({ + languageItemCode: C.string, + geolCode: C.string, + name: C.string, + nameDe: C.string, + nameFr: C.string, + nameRm: C.string, + nameIt: C.string, + nameEn: C.string, + description: C.string, + descriptionDe: C.string, + descriptionFr: C.string, + descriptionRm: C.string, + descriptionIt: C.string, + descriptionEn: C.string, + }), + }) + ), assetContacts: C.array( C.struct({ role: AssetContactRole, diff --git a/libs/shared/src/lib/models/asset-edit.ts b/libs/shared/src/lib/models/asset-edit.ts index 30cf7691..f35ec8db 100644 --- a/libs/shared/src/lib/models/asset-edit.ts +++ b/libs/shared/src/lib/models/asset-edit.ts @@ -1,5 +1,6 @@ import { struct } from 'fp-ts/Eq'; import { Eq as eqNumber } from 'fp-ts/number'; +import { Eq as eqString } from 'fp-ts/string'; import * as C from 'io-ts/Codec'; import { CT } from '@asset-sg/core'; @@ -29,11 +30,18 @@ export const ContactEdit = C.struct({ }); export interface ContactEdit extends C.TypeOf {} +export const AssetLanguageEdit = C.struct({ + languageItemCode: C.string, +}); +export interface AssetLanguageEdit extends C.TypeOf {} +export const eqAssetLanguageEdit = struct({ + languageItemCode: eqString, +}); + export const AssetContactEdit = C.struct({ role: AssetContactRole, contactId: C.number, }); - export interface AssetContactEdit extends C.TypeOf {} export const eqAssetContactEdit = struct({ role: eqAssetContactRole, @@ -55,7 +63,6 @@ export const BaseAssetEditDetail = { internalUse: AssetUsage, assetKindItemCode: C.string, assetFormatItemCode: C.string, - languageItemCode: C.string, isNatRel: C.boolean, sgsId: C.nullable(C.number), geolDataInfo: C.nullable(C.string), @@ -63,6 +70,7 @@ export const BaseAssetEditDetail = { geolAuxDataInfo: C.nullable(C.string), municipality: C.nullable(C.string), ids: C.array(C.struct({ idId: C.number, id: C.string, description: C.string })), + assetLanguages: C.array(AssetLanguageEdit), assetContacts: C.array(AssetContactEdit), manCatLabelRefs: C.array(C.string), assetFormatCompositions: C.array(C.string), diff --git a/libs/shared/src/lib/models/elastic-search-asset.ts b/libs/shared/src/lib/models/elastic-search-asset.ts index d8041ee7..9ea4455e 100644 --- a/libs/shared/src/lib/models/elastic-search-asset.ts +++ b/libs/shared/src/lib/models/elastic-search-asset.ts @@ -10,7 +10,7 @@ export interface ElasticSearchAsset { sgsId: number | null createDate: DateId assetKindItemCode: string - languageItemCode: string + languageItemCodes: string[] usageCode: ElasticSearchUsageCode authorIds: number[] contactNames: string[] diff --git a/libs/shared/src/lib/models/patch-asset.ts b/libs/shared/src/lib/models/patch-asset.ts index 68216cba..463b3e42 100644 --- a/libs/shared/src/lib/models/patch-asset.ts +++ b/libs/shared/src/lib/models/patch-asset.ts @@ -2,7 +2,7 @@ import * as C from 'io-ts/Codec'; import { CT } from '@asset-sg/core'; -import { AssetContactEdit } from './asset-edit'; +import { AssetContactEdit, AssetLanguageEdit } from './asset-edit'; import { AssetUsage } from './asset-usage'; import { DateId } from './DateStruct'; @@ -15,10 +15,10 @@ export const PatchAsset = C.struct({ internalUse: AssetUsage, assetKindItemCode: C.string, assetFormatItemCode: C.string, - languageItemCode: C.string, isNatRel: C.boolean, manCatLabelRefs: C.array(C.string), typeNatRels: C.array(C.string), + assetLanguages: C.array(AssetLanguageEdit), assetContacts: C.array(AssetContactEdit), ids: C.array( C.struct({ diff --git a/test/setup-db.ts b/test/setup-db.ts index 6655dd39..96280ebc 100644 --- a/test/setup-db.ts +++ b/test/setup-db.ts @@ -41,6 +41,7 @@ export const setupDB = async (prisma: PrismaClient): Promise => { export const clearPrismaAssets = async (prisma: PrismaClient): Promise => { await prisma.manCatLabelRef.deleteMany(); await prisma.assetContact.deleteMany(); + await prisma.assetLanguage.deleteMany(); await prisma.contact.deleteMany(); await prisma.id.deleteMany(); await prisma.typeNatRel.deleteMany();