diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 67feb82fac..43eb66e9d7 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -61,7 +61,6 @@ function serializeSpec(spec: Entity['spec']): DbEntitiesRow['spec'] { function toEntityRow( locationId: string | undefined, entity: Entity, - normalize: (value: string) => string, ): DbEntitiesRow { return { id: entity.metadata.uid!, @@ -72,9 +71,6 @@ function toEntityRow( kind: entity.kind, name: entity.metadata.name, namespace: entity.metadata.namespace || null, - kind_normalized: normalize(entity.kind), - name_normalized: normalize(entity.metadata.name), - namespace_normalized: normalize(entity.metadata.namespace || ''), metadata: serializeMetadata(entity.metadata), spec: serializeSpec(entity.spec), }; @@ -162,6 +158,8 @@ export class CommonDatabase implements Database { throw new InputError('May not specify generation for new entities'); } + await this.ensureNoSimilarNames(tx, request.entity); + const newEntity = lodash.cloneDeep(request.entity); newEntity.metadata = { ...newEntity.metadata, @@ -170,7 +168,7 @@ export class CommonDatabase implements Database { generation: 1, }; - const newRow = toEntityRow(request.locationId, newEntity, this.normalize); + const newRow = toEntityRow(request.locationId, newEntity); await tx('entities').insert(newRow); await this.updateEntitiesSearch(tx, newRow.id, newEntity); @@ -259,9 +257,11 @@ export class CommonDatabase implements Database { } } + await this.ensureNoSimilarNames(tx, newEntity); + // Store the updated entity; select on the old etag to ensure that we do // not lose to another writer - const newRow = toEntityRow(request.locationId, newEntity, this.normalize); + const newRow = toEntityRow(request.locationId, newEntity); const updatedRows = await tx('entities') .where({ id: oldRow.id, etag: oldRow.etag }) .update(newRow); @@ -451,4 +451,46 @@ export class CommonDatabase implements Database { // we got around to writing the entries } } + + private async ensureNoSimilarNames( + tx: Knex.Transaction, + data: Entity, + ): Promise { + const newKind = data.kind; + const newName = data.metadata.name; + const newNamespace = data.metadata.namespace; + const newKindNorm = this.normalize(newKind); + const newNameNorm = this.normalize(newName); + const newNamespaceNorm = this.normalize(newNamespace || ''); + + for (const item of await this.entities(tx)) { + if (data.metadata.uid === item.entity.metadata.uid) { + continue; + } + + const oldKind = item.entity.kind; + const oldName = item.entity.metadata.name; + const oldNamespace = item.entity.metadata.namespace; + const oldKindNorm = this.normalize(oldKind); + const oldNameNorm = this.normalize(oldName); + const oldNamespaceNorm = this.normalize(oldNamespace || ''); + + if ( + oldKindNorm === newKindNorm && + oldNameNorm === newNameNorm && + oldNamespaceNorm === newNamespaceNorm + ) { + // Only throw if things were actually different - for completely equal + // things, we let the database handle the conflict + if ( + oldKind !== newKind || + oldName !== newName || + oldNamespace !== newNamespace + ) { + const message = `Kind, namespace, name are too similar to an existing entity`; + throw new ConflictError(message); + } + } + } + } } diff --git a/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts b/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts deleted file mode 100644 index 38ed7f6047..0000000000 --- a/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as Knex from 'knex'; - -export async function up(knex: Knex): Promise { - return knex.schema.alterTable('entities', table => { - // these are added as nullable because sqlite does not support alter column - table - .string('kind_normalized') - .nullable() - .comment('The kind field of the entity, normalized'); - table - .string('name_normalized') - .nullable() - .comment('The metadata.name field of the entity, normalized'); - table - .string('namespace_normalized') - .nullable() - .comment('The metadata.namespace field of the entity, normalized'); - table.dropUnique([], 'entities_unique_name'); - table.unique( - ['kind_normalized', 'name_normalized', 'namespace_normalized'], - 'entities_unique_name_normalized', - ); - }); -} - -export async function down(knex: Knex): Promise { - return knex.schema.alterTable('entities', table => { - table.dropUnique([], 'entities_unique_name_normalized'); - table.unique(['kind', 'name', 'namespace'], 'entities_unique_name'); - table.dropColumns( - 'kind_normalized', - 'name_normalized', - 'namespace_normalized', - ); - }); -} diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 1cf949c4c2..fc81e124ba 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -23,9 +23,6 @@ export type DbEntitiesRow = { kind: string; name: string | null; namespace: string | null; - kind_normalized: string; - name_normalized: string; - namespace_normalized: string; etag: string; generation: number; metadata: string;