Skip to content

Commit 42042d9

Browse files
committed
fix: fix lookup filter by the projection
1 parent ee60e85 commit 42042d9

File tree

2 files changed

+196
-6
lines changed

2 files changed

+196
-6
lines changed

‎apps/nestjs-backend/src/features/record/query-builder/field-cte-visitor.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,12 +1882,12 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
18821882
const { fkHostTableName, selfKeyName, foreignKeyName, relationship } = options;
18831883

18841884
// Determine which lookup/rollup fields are actually needed from this link
1885-
let lookupFields = linkField.getLookupFields(this.table);
1886-
let rollupFields = linkField.getRollupFields(this.table);
1887-
if (this.filteredIdSet) {
1888-
lookupFields = lookupFields.filter((f) => this.filteredIdSet!.has(f.id));
1889-
rollupFields = rollupFields.filter((f) => this.filteredIdSet!.has(f.id));
1890-
}
1885+
// NOTE: We intentionally do not filter by the projection (filteredIdSet). A lookup that
1886+
// is not part of the direct projection can still be an intermediate dependency for
1887+
// another lookup/rollup selected via a different table. Filtering here can therefore
1888+
// omit required CTE columns, leading to generated SQL that references non-existent
1889+
const lookupFields = linkField.getLookupFields(this.table);
1890+
const rollupFields = linkField.getRollupFields(this.table);
18911891

18921892
// Pre-generate nested CTEs limited to selected lookup/rollup dependencies
18931893
this.generateNestedForeignCtesIfNeeded(

‎apps/nestjs-backend/test/lookup.e2e-spec.ts‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,196 @@ describe('OpenAPI Lookup field (e2e)', () => {
675675
});
676676
});
677677

678+
describe('nested lookup dependencies', () => {
679+
let usersTable: ITableFullVo;
680+
let projectsTable: ITableFullVo;
681+
let tasksTable: ITableFullVo;
682+
let userNameField: IFieldVo;
683+
let projectNameField: IFieldVo;
684+
let taskNameField: IFieldVo;
685+
let projectOwnerLookupField: IFieldVo;
686+
let taskOwnerLookupField: IFieldVo;
687+
let projectLinkFieldId: string;
688+
let taskLinkFieldId: string;
689+
let userRecordId: string;
690+
let projectRecordId: string;
691+
let taskRecordId: string;
692+
693+
const refreshFields = async (table: ITableFullVo) => {
694+
table.fields = await getFields(table.id);
695+
};
696+
697+
const getFieldByName = (fields: IFieldVo[], name: string) => {
698+
const field = fields.find((f) => f.name === name);
699+
if (!field) {
700+
throw new Error(`Field ${name} not found`);
701+
}
702+
return field;
703+
};
704+
705+
beforeAll(async () => {
706+
usersTable = await createTable(baseId, {
707+
name: 'lookup-nested-users',
708+
fields: [
709+
{
710+
name: 'User Name',
711+
type: FieldType.SingleLineText,
712+
options: {},
713+
},
714+
],
715+
});
716+
717+
projectsTable = await createTable(baseId, {
718+
name: 'lookup-nested-projects',
719+
fields: [
720+
{
721+
name: 'Project Name',
722+
type: FieldType.SingleLineText,
723+
options: {},
724+
},
725+
],
726+
});
727+
728+
tasksTable = await createTable(baseId, {
729+
name: 'lookup-nested-tasks',
730+
fields: [
731+
{
732+
name: 'Task Name',
733+
type: FieldType.SingleLineText,
734+
options: {},
735+
},
736+
],
737+
});
738+
739+
await refreshFields(usersTable);
740+
await refreshFields(projectsTable);
741+
await refreshFields(tasksTable);
742+
743+
userNameField = getFieldByName(usersTable.fields, 'User Name');
744+
projectNameField = getFieldByName(projectsTable.fields, 'Project Name');
745+
taskNameField = getFieldByName(tasksTable.fields, 'Task Name');
746+
747+
const projectLinkField = await createField(projectsTable.id, {
748+
name: 'Project -> User',
749+
type: FieldType.Link,
750+
options: {
751+
relationship: Relationship.ManyOne,
752+
foreignTableId: usersTable.id,
753+
},
754+
});
755+
projectLinkFieldId = projectLinkField.id;
756+
757+
await refreshFields(projectsTable);
758+
await refreshFields(usersTable);
759+
760+
projectOwnerLookupField = await createField(projectsTable.id, {
761+
name: 'Project Owner (lookup)',
762+
type: FieldType.SingleLineText,
763+
isLookup: true,
764+
lookupOptions: {
765+
foreignTableId: usersTable.id,
766+
linkFieldId: projectLinkFieldId,
767+
lookupFieldId: userNameField.id,
768+
} as ILookupOptionsRo,
769+
});
770+
771+
await refreshFields(projectsTable);
772+
773+
const taskLinkField = await createField(tasksTable.id, {
774+
name: 'Task -> Project',
775+
type: FieldType.Link,
776+
options: {
777+
relationship: Relationship.ManyOne,
778+
foreignTableId: projectsTable.id,
779+
},
780+
});
781+
taskLinkFieldId = taskLinkField.id;
782+
783+
await refreshFields(tasksTable);
784+
await refreshFields(projectsTable);
785+
786+
taskOwnerLookupField = await createField(tasksTable.id, {
787+
name: 'Task Project Owner (lookup)',
788+
type: FieldType.SingleLineText,
789+
isLookup: true,
790+
lookupOptions: {
791+
foreignTableId: projectsTable.id,
792+
linkFieldId: taskLinkFieldId,
793+
lookupFieldId: projectOwnerLookupField.id,
794+
} as ILookupOptionsRo,
795+
});
796+
797+
await refreshFields(tasksTable);
798+
799+
const createdUsers = await createRecords(usersTable.id, {
800+
fieldKeyType: FieldKeyType.Id,
801+
records: [
802+
{
803+
fields: {
804+
[userNameField.id]: 'Alice',
805+
},
806+
},
807+
],
808+
});
809+
userRecordId = createdUsers.records[0].id;
810+
811+
const createdProjects = await createRecords(projectsTable.id, {
812+
fieldKeyType: FieldKeyType.Id,
813+
records: [
814+
{
815+
fields: {
816+
[projectNameField.id]: 'Project Alpha',
817+
},
818+
},
819+
],
820+
});
821+
projectRecordId = createdProjects.records[0].id;
822+
823+
await updateRecordByApi(projectsTable.id, projectRecordId, projectLinkFieldId, {
824+
id: userRecordId,
825+
});
826+
827+
const createdTasks = await createRecords(tasksTable.id, {
828+
fieldKeyType: FieldKeyType.Id,
829+
records: [
830+
{
831+
fields: {
832+
[taskNameField.id]: 'Task 1',
833+
},
834+
},
835+
],
836+
});
837+
taskRecordId = createdTasks.records[0].id;
838+
839+
await updateRecordByApi(tasksTable.id, taskRecordId, taskLinkFieldId, {
840+
id: projectRecordId,
841+
});
842+
});
843+
844+
afterAll(async () => {
845+
await permanentDeleteTable(baseId, tasksTable.id);
846+
await permanentDeleteTable(baseId, projectsTable.id);
847+
await permanentDeleteTable(baseId, usersTable.id);
848+
});
849+
850+
it('should recompute nested lookup values after relinking', async () => {
851+
let taskRecord = await getRecord(tasksTable.id, taskRecordId);
852+
expect(taskRecord.fields[taskOwnerLookupField.id]).toEqual('Alice');
853+
854+
await updateRecordByApi(tasksTable.id, taskRecordId, taskLinkFieldId, null);
855+
856+
taskRecord = await getRecord(tasksTable.id, taskRecordId);
857+
expect(taskRecord.fields[taskOwnerLookupField.id]).toBeUndefined();
858+
859+
await updateRecordByApi(tasksTable.id, taskRecordId, taskLinkFieldId, {
860+
id: projectRecordId,
861+
});
862+
863+
taskRecord = await getRecord(tasksTable.id, taskRecordId);
864+
expect(taskRecord.fields[taskOwnerLookupField.id]).toEqual('Alice');
865+
});
866+
});
867+
678868
describe('lookup filter', () => {
679869
let table1: ITableFullVo;
680870
let table2: ITableFullVo;

0 commit comments

Comments
 (0)