Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1906,7 +1906,7 @@ export class FieldSupplementService {
select: { name: true, baseId: true },
});

const fieldName = await this.uniqFieldName(tableId, tableName);
const fieldName = await this.uniqFieldName(field.options.foreignTableId, tableName);

// lookup field id is the primary field of the table to which it is linked
const { id: lookupFieldId } = await prisma.field.findFirstOrThrow({
Expand Down
91 changes: 91 additions & 0 deletions apps/nestjs-backend/test/field-duplicate.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
initApp,
createRecords,
getRecords,
convertField,
} from './utils/init-app';

describe('OpenAPI FieldOpenApiController for duplicate field (e2e)', () => {
Expand Down Expand Up @@ -598,6 +599,96 @@ describe('OpenAPI FieldOpenApiController for duplicate field (e2e)', () => {
await permanentDeleteTable(baseId, table.id);
await permanentDeleteTable(baseId, subTable.id);
});

it('keeps symmetric field names unique after converting a duplicated one-way link back to two-way', async () => {
let sourceTable: ITableFullVo | undefined;
let foreignTable: ITableFullVo | undefined;

try {
sourceTable = await createTable(baseId, {
name: 'dup_link_name_source',
fields: [{ name: 'Name', type: FieldType.SingleLineText, isPrimary: true } as IFieldRo],
});

foreignTable = await createTable(baseId, {
name: 'dup_link_name_foreign',
fields: [{ name: 'Title', type: FieldType.SingleLineText, isPrimary: true } as IFieldRo],
});

const foreignPrimaryFieldId = foreignTable.fields.find((field) => field.isPrimary)?.id;
expect(foreignPrimaryFieldId).toBeDefined();
if (!foreignPrimaryFieldId) {
throw new Error('Missing foreign primary field');
}

const originalField = (
await createField(sourceTable.id, {
type: FieldType.Link,
name: 'Customer',
options: {
relationship: Relationship.ManyMany,
foreignTableId: foreignTable.id,
lookupFieldId: foreignPrimaryFieldId,
isOneWay: false,
},
})
).data;

const originalSymmetricFieldId = (originalField.options as ILinkFieldOptions)
.symmetricFieldId;
expect(originalSymmetricFieldId).toBeDefined();
if (!originalSymmetricFieldId) {
throw new Error('Missing original symmetric field');
}

const duplicatedField = (
await duplicateField(sourceTable.id, originalField.id, {
name: 'Customer Copy',
})
).data;

expect((duplicatedField.options as ILinkFieldOptions).isOneWay).toBe(true);
expect((duplicatedField.options as ILinkFieldOptions).symmetricFieldId).toBeUndefined();

const convertedField = await convertField(sourceTable.id, duplicatedField.id, {
type: FieldType.Link,
name: duplicatedField.name,
options: {
relationship: Relationship.ManyMany,
foreignTableId: foreignTable.id,
lookupFieldId: foreignPrimaryFieldId,
isOneWay: false,
},
});

const convertedSymmetricFieldId = (convertedField.options as ILinkFieldOptions)
.symmetricFieldId;
expect(convertedSymmetricFieldId).toBeDefined();
if (!convertedSymmetricFieldId) {
throw new Error('Missing converted symmetric field');
}

const foreignFields = (await getFields(foreignTable.id)).data;
const originalSymmetricField = foreignFields.find(
(field) => field.id === originalSymmetricFieldId
);
const convertedSymmetricField = foreignFields.find(
(field) => field.id === convertedSymmetricFieldId
);

expect(originalSymmetricField?.name).toBeDefined();
expect(convertedSymmetricField?.name).toBeDefined();
expect(originalSymmetricField?.name).not.toBe(convertedSymmetricField?.name);
expect(new Set([originalSymmetricField?.name, convertedSymmetricField?.name]).size).toBe(2);
} finally {
if (sourceTable) {
await permanentDeleteTable(baseId, sourceTable.id);
}
if (foreignTable) {
await permanentDeleteTable(baseId, foreignTable.id);
}
}
});
});

describe('duplicate link field should copy cell data', () => {
Expand Down
117 changes: 117 additions & 0 deletions packages/v2/e2e/src/duplicateField.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,123 @@ describe('duplicateField', () => {
await ctx.deleteTable(table.id);
});

it('keeps symmetric field names unique after converting a duplicated one-way link back to two-way', async () => {
let hostTableId: string | undefined;
let foreignTableId: string | undefined;

try {
const hostTable = await ctx.createTable({
baseId: ctx.baseId,
name: `DupLinkNameHost-${Date.now()}`,
fields: [{ type: 'singleLineText', name: 'Name', isPrimary: true }],
});
hostTableId = hostTable.id;

const foreignTable = await ctx.createTable({
baseId: ctx.baseId,
name: `DupLinkNameForeign-${Date.now()}`,
fields: [{ type: 'singleLineText', name: 'Title', isPrimary: true }],
});
foreignTableId = foreignTable.id;

const foreignPrimaryFieldId = foreignTable.fields.find((field) => field.isPrimary)?.id;
expect(foreignPrimaryFieldId).toBeTruthy();
if (!foreignPrimaryFieldId) return;

const hostTableWithLink = await ctx.createField({
baseId: ctx.baseId,
tableId: hostTable.id,
field: {
type: 'link',
name: 'Customer',
options: {
foreignTableId: foreignTable.id,
relationship: 'manyMany',
lookupFieldId: foreignPrimaryFieldId,
isOneWay: false,
},
},
});

const originalField = hostTableWithLink.fields.find((field) => field.name === 'Customer');
expect(originalField).toBeTruthy();
if (!originalField) return;

const originalSymmetricFieldId = (originalField.options as { symmetricFieldId?: string })
.symmetricFieldId;
expect(originalSymmetricFieldId).toBeTruthy();
if (!originalSymmetricFieldId) return;

const duplicateResponse = await fetch(`${ctx.baseUrl}/tables/duplicateField`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
baseId: ctx.baseId,
tableId: hostTable.id,
fieldId: originalField.id,
includeRecordValues: true,
newFieldName: 'Customer Copy',
}),
});

expect(duplicateResponse.status).toBe(200);
const duplicateRaw = await duplicateResponse.json();
const duplicateParsed = duplicateFieldOkResponseSchema.safeParse(duplicateRaw);
expect(duplicateParsed.success).toBe(true);
expect(duplicateParsed.success && duplicateParsed.data.ok).toBe(true);
if (!duplicateParsed.success || !duplicateParsed.data.ok) return;

const duplicatedFieldId = duplicateParsed.data.data.newFieldId;

const duplicatedTable = await ctx.getTableById(hostTable.id);
const duplicatedField = duplicatedTable.fields.find(
(field) => field.id === duplicatedFieldId
);
expect(duplicatedField?.type).toBe('link');
expect((duplicatedField?.options as { isOneWay?: boolean })?.isOneWay).toBe(true);

const updatedTable = await ctx.updateField({
tableId: hostTable.id,
fieldId: duplicatedFieldId,
field: {
options: {
foreignTableId: foreignTable.id,
relationship: 'manyMany',
lookupFieldId: foreignPrimaryFieldId,
isOneWay: false,
},
},
});

const updatedField = updatedTable.fields.find((field) => field.id === duplicatedFieldId);
const newSymmetricFieldId = (updatedField?.options as { symmetricFieldId?: string })
?.symmetricFieldId;

expect(newSymmetricFieldId).toBeTruthy();
if (!newSymmetricFieldId) return;

const foreignTableAfter = await ctx.getTableById(foreignTable.id);
const originalSymmetricField = foreignTableAfter.fields.find(
(field) => field.id === originalSymmetricFieldId
);
const newSymmetricField = foreignTableAfter.fields.find(
(field) => field.id === newSymmetricFieldId
);

expect(originalSymmetricField?.name).toBeTruthy();
expect(newSymmetricField?.name).toBeTruthy();
expect(originalSymmetricField?.name).not.toBe(newSymmetricField?.name);
expect(new Set([originalSymmetricField?.name, newSymmetricField?.name]).size).toBe(2);
} finally {
if (hostTableId) {
await ctx.deleteTable(hostTableId);
}
if (foreignTableId) {
await ctx.deleteTable(foreignTableId);
}
}
});

it('duplicates all field types with unique dbFieldName', async () => {
let hostTableId: string | undefined;
let foreignTableId: string | undefined;
Expand Down
Loading