Skip to content

Commit

Permalink
fix: missing cross-base link fields when update dbTableName (#1200)
Browse files Browse the repository at this point in the history
  • Loading branch information
tea-artist authored Dec 29, 2024
1 parent 12ec0e8 commit 6dacd39
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 49 deletions.
6 changes: 5 additions & 1 deletion apps/nestjs-backend/src/db-provider/db.provider.interface.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
24 changes: 23 additions & 1 deletion apps/nestjs-backend/src/db-provider/postgres.provider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
}
}
24 changes: 23 additions & 1 deletion apps/nestjs-backend/src/db-provider/sqlite.provider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
55 changes: 55 additions & 0 deletions apps/nestjs-backend/test/link-api.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 6dacd39

Please sign in to comment.