diff --git a/packages/backend/migration/1750591589187-registry-unique-constraints.js b/packages/backend/migration/1750591589187-registry-unique-constraints.js new file mode 100644 index 0000000000..e9fa6f609d --- /dev/null +++ b/packages/backend/migration/1750591589187-registry-unique-constraints.js @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: dakkar and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RegistryUniqueConstraints1750591589187 { + async up(queryRunner) { + await queryRunner.query(`DELETE FROM "registry_item" WHERE "id" IN ( +SELECT t."id" FROM ( +SELECT *, ROW_NUMBER() OVER (PARTITION BY "userId","key","scope","domain" ORDER BY "updatedAt" DESC) rn +FROM "registry_item" +) t WHERE t.rn>1)`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9c48d580287308f8c1f674946" ON "registry_item" ("userId", "key", "scope", "domain") NULLS NOT DISTINCT`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d9c48d580287308f8c1f674946"`); + } +} diff --git a/packages/backend/src/core/RegistryApiService.ts b/packages/backend/src/core/RegistryApiService.ts index 2c8877d8a8..2c7ad4026d 100644 --- a/packages/backend/src/core/RegistryApiService.ts +++ b/packages/backend/src/core/RegistryApiService.ts @@ -27,25 +27,9 @@ export class RegistryApiService { public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) { // TODO: 作成できるキーの数を制限する - const query = this.registryItemsRepository.createQueryBuilder('item'); - if (domain) { - query.where('item.domain = :domain', { domain: domain }); - } else { - query.where('item.domain IS NULL'); - } - query.andWhere('item.userId = :userId', { userId: userId }); - query.andWhere('item.key = :key', { key: key }); - query.andWhere('item.scope = :scope', { scope: scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await this.registryItemsRepository.update(existingItem.id, { - updatedAt: new Date(), - value: value, - }); - } else { - await this.registryItemsRepository.insert({ + await this.registryItemsRepository.createQueryBuilder('item') + .insert() + .values({ id: this.idService.gen(), updatedAt: new Date(), userId: userId, @@ -53,8 +37,13 @@ export class RegistryApiService { scope: scope, key: key, value: value, - }); - } + }) + .orUpdate( + ['updatedAt', 'value'], + ['userId', 'key', 'scope', 'domain'], + { upsertType: 'on-conflict-do-update' } + ) + .execute(); if (domain == null) { // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする diff --git a/packages/backend/src/models/RegistryItem.ts b/packages/backend/src/models/RegistryItem.ts index 335e8b9eab..ceb0305bb4 100644 --- a/packages/backend/src/models/RegistryItem.ts +++ b/packages/backend/src/models/RegistryItem.ts @@ -7,8 +7,8 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { id } from './util/id.js'; import { MiUser } from './User.js'; -// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい @Entity('registry_item') +@Index(['userId', 'key', 'scope', 'domain'], { unique: true }) export class MiRegistryItem { @PrimaryColumn(id()) public id: string;