diff --git a/apps/nestjs-backend/src/db-provider/db.provider.interface.ts b/apps/nestjs-backend/src/db-provider/db.provider.interface.ts index 17baa9e42c..a289c2188e 100644 --- a/apps/nestjs-backend/src/db-provider/db.provider.interface.ts +++ b/apps/nestjs-backend/src/db-provider/db.provider.interface.ts @@ -1,4 +1,4 @@ -import type { DriverClient, IFilter, ISortItem } from '@teable/core'; +import type { DriverClient, IFilter, ILookupOptionsVo, ISortItem } from '@teable/core'; import type { Prisma } from '@teable/db-main-prisma'; import type { IAggregationField, ISearchIndexByQueryRo } from '@teable/openapi'; import type { Knex } from 'knex'; @@ -162,4 +162,8 @@ export interface IDbProvider { qb: Knex.QueryBuilder, props: ICalendarDailyCollectionQueryProps ): Knex.QueryBuilder; + + lookupOptionsQuery(optionsKey: keyof ILookupOptionsVo, value: string): string; + + optionsQuery(optionsKey: string, value: string): string; } diff --git a/apps/nestjs-backend/src/db-provider/postgres.provider.ts b/apps/nestjs-backend/src/db-provider/postgres.provider.ts index 93ee219224..7df49319e8 100644 --- a/apps/nestjs-backend/src/db-provider/postgres.provider.ts +++ b/apps/nestjs-backend/src/db-provider/postgres.provider.ts @@ -1,6 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { Logger } from '@nestjs/common'; -import type { IFilter, ISortItem } from '@teable/core'; +import type { IFilter, ILookupOptionsVo, ISortItem } from '@teable/core'; import { DriverClient } from '@teable/core'; import type { PrismaClient } from '@teable/db-main-prisma'; import type { IAggregationField, ISearchIndexByQueryRo } from '@teable/openapi'; @@ -418,4 +418,26 @@ export class PostgresProvider implements IDbProvider { .groupBy('dates.date') .orderBy('dates.date', 'asc'); } + + // select id and lookup_options for "field" table options is a json saved in string format, match optionsKey and value + // please use json method in postgres + lookupOptionsQuery(optionsKey: keyof ILookupOptionsVo, value: string): string { + return this.knex('field') + .select({ + id: 'id', + lookupOptions: 'lookup_options', + }) + .whereRaw(`lookup_options::json->>'${optionsKey}' = ?`, [value]) + .toQuery(); + } + + optionsQuery(optionsKey: string, value: string): string { + return this.knex('field') + .select({ + id: 'id', + options: 'options', + }) + .whereRaw(`options::json->>'${optionsKey}' = ?`, [value]) + .toQuery(); + } } diff --git a/apps/nestjs-backend/src/db-provider/sqlite.provider.ts b/apps/nestjs-backend/src/db-provider/sqlite.provider.ts index 2bc80bbb1d..03a0113304 100644 --- a/apps/nestjs-backend/src/db-provider/sqlite.provider.ts +++ b/apps/nestjs-backend/src/db-provider/sqlite.provider.ts @@ -1,6 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { Logger } from '@nestjs/common'; -import type { IFilter, ISortItem } from '@teable/core'; +import type { IFilter, ILookupOptionsVo, ISortItem } from '@teable/core'; import { DriverClient } from '@teable/core'; import type { PrismaClient } from '@teable/db-main-prisma'; import type { IAggregationField, ISearchIndexByQueryRo } from '@teable/openapi'; @@ -375,4 +375,26 @@ export class SqliteProvider implements IDbProvider { .groupBy('d.date') .orderBy('d.date', 'asc'); } + + // select id and lookup_options for "field" table options is a json saved in string format, match optionsKey and value + // please use json method in sqlite + lookupOptionsQuery(optionsKey: keyof ILookupOptionsVo, value: string): string { + return this.knex('field') + .select({ + id: 'id', + lookupOptions: 'lookup_options', + }) + .whereRaw(`json_extract(lookup_options, '$."${optionsKey}"') = ?`, [value]) + .toQuery(); + } + + optionsQuery(optionsKey: string, value: string): string { + return this.knex('field') + .select({ + id: 'id', + options: 'options', + }) + .whereRaw(`json_extract(options, '$."${optionsKey}"') = ?`, [value]) + .toQuery(); + } } diff --git a/apps/nestjs-backend/src/features/table/open-api/table-open-api.service.ts b/apps/nestjs-backend/src/features/table/open-api/table-open-api.service.ts index b7c2948c09..b449204cc3 100644 --- a/apps/nestjs-backend/src/features/table/open-api/table-open-api.service.ts +++ b/apps/nestjs-backend/src/features/table/open-api/table-open-api.service.ts @@ -469,55 +469,39 @@ export class TableOpenApiService { throw new NotFoundException(`table ${tableId} not found`); }); - const linkFieldsRaw = await this.prismaService.field.findMany({ - where: { table: { baseId }, type: FieldType.Link }, - select: { id: true, options: true }, - }); - - const relationalFieldsRaw = await this.prismaService.field.findMany({ - where: { table: { baseId }, lookupOptions: { not: null } }, - select: { id: true, lookupOptions: true }, - }); + const linkFieldsQuery = this.dbProvider.optionsQuery('fkHostTableName', oldDbTableName); + const lookupFieldsQuery = this.dbProvider.lookupOptionsQuery('fkHostTableName', oldDbTableName); await this.prismaService.$tx(async (prisma) => { - await Promise.all( - linkFieldsRaw - .map((field) => ({ - ...field, - options: JSON.parse(field.options as string) as ILinkFieldOptions, - })) - .filter((field) => { - return field.options.fkHostTableName === oldDbTableName; - }) - .map((field) => { - return prisma.field.update({ - where: { id: field.id }, - data: { options: JSON.stringify({ ...field.options, fkHostTableName: dbTableName }) }, - }); - }) - ); + const linkFieldsRaw = + await this.prismaService.$queryRawUnsafe<{ id: string; options: string }[]>( + linkFieldsQuery + ); + const lookupFieldsRaw = + await this.prismaService.$queryRawUnsafe<{ id: string; lookupOptions: string }[]>( + lookupFieldsQuery + ); - await Promise.all( - relationalFieldsRaw - .map((field) => ({ - ...field, - lookupOptions: JSON.parse(field.lookupOptions as string) as ILookupOptionsVo, - })) - .filter((field) => { - return field.lookupOptions.fkHostTableName === oldDbTableName; - }) - .map((field) => { - return prisma.field.update({ - where: { id: field.id }, - data: { - lookupOptions: JSON.stringify({ - ...field.lookupOptions, - fkHostTableName: dbTableName, - }), - }, - }); - }) - ); + for (const field of linkFieldsRaw) { + const options = JSON.parse(field.options as string) as ILinkFieldOptions; + await prisma.field.update({ + where: { id: field.id }, + data: { options: JSON.stringify({ ...options, fkHostTableName: dbTableName }) }, + }); + } + + for (const field of lookupFieldsRaw) { + const lookupOptions = JSON.parse(field.lookupOptions as string) as ILookupOptionsVo; + await prisma.field.update({ + where: { id: field.id }, + data: { + lookupOptions: JSON.stringify({ + ...lookupOptions, + fkHostTableName: dbTableName, + }), + }, + }); + } await this.tableService.updateTable(baseId, tableId, { dbTableName }); const renameSql = this.dbProvider.renameTableName(oldDbTableName, dbTableName); diff --git a/apps/nestjs-backend/test/link-api.e2e-spec.ts b/apps/nestjs-backend/test/link-api.e2e-spec.ts index cb26803ffe..67f33ef30d 100644 --- a/apps/nestjs-backend/test/link-api.e2e-spec.ts +++ b/apps/nestjs-backend/test/link-api.e2e-spec.ts @@ -2997,6 +2997,61 @@ describe('OpenAPI link (e2e)', () => { ); expect((symUpdatedLinkField.options as ILinkFieldOptions).baseId).toEqual(baseId); }); + + it('should correct update db table name when link field is cross base', async () => { + const linkFieldRo: IFieldRo = { + name: 'link field', + type: FieldType.Link, + options: { + baseId: baseId2, + relationship: Relationship.ManyOne, + foreignTableId: table2.id, + }, + }; + + const linkField = await createField(table1.id, linkFieldRo); + + const symLinkField = await getField( + table2.id, + (linkField.options as ILinkFieldOptions).symmetricFieldId as string + ); + + expect((linkField.options as ILinkFieldOptions).fkHostTableName).toEqual(table1.dbTableName); + expect((symLinkField.options as ILinkFieldOptions).fkHostTableName).toEqual( + table1.dbTableName + ); + + const lookupFieldRo: IFieldRo = { + type: FieldType.SingleLineText, + isLookup: true, + lookupOptions: { + foreignTableId: table1.id, + lookupFieldId: table1.fields[0].id, + linkFieldId: symLinkField.id, + }, + }; + + const lookupField = await createField(table2.id, lookupFieldRo); + + await updateDbTableName(baseId, table1.id, { dbTableName: 'newAwesomeName' }); + const newTable1 = await getTable(baseId, table1.id); + const updatedLink1 = await getField(table1.id, linkField.id); + const updatedLink2 = await getField(table2.id, symLinkField.id); + const updatedLookupField = await getField(table2.id, lookupField.id); + + expect(newTable1.dbTableName.split(/[._]/)).toEqual(['bseTestBaseId', 'newAwesomeName']); + expect((updatedLink1.options as ILinkFieldOptions).fkHostTableName.split(/[._]/)).toEqual([ + 'bseTestBaseId', + 'newAwesomeName', + ]); + expect((updatedLink2.options as ILinkFieldOptions).fkHostTableName.split(/[._]/)).toEqual([ + 'bseTestBaseId', + 'newAwesomeName', + ]); + expect( + (updatedLookupField.lookupOptions as ILookupOptionsVo).fkHostTableName.split(/[._]/) + ).toEqual(['bseTestBaseId', 'newAwesomeName']); + }); }); describe('lookup a link field cross 2 table', () => {