Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Fix record identity using link attribute #288

Merged
merged 5 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
235 changes: 235 additions & 0 deletions apps/core/src/__tests__/e2e/api/records/recordIdentity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright LEAV Solutions 2017
// This file is released under LGPL V3
// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt
import {AttributeFormats, AttributeTypes} from '../../../../_types/attribute';
import {AttributeCondition} from '../../../../_types/record';
import {
gqlAddElemToTree,
gqlSaveAttribute,
gqlSaveLibrary,
gqlSaveTree,
gqlSaveValue,
makeGraphQlCall
} from '../e2eUtils';

describe('Record identity', () => {
// For regular identity (=own attribute)
const testLibraryId = 'record_identity_library_test';
const testLibraryTypeName = 'recordIdentityLibraryTest';
let recordId;

// For identity through link attribute
const testLinkedIdentityLibraryId = 'record_identity_test_linked_identity';
const testLinkedIdentityLibraryTypeName = 'recordIdentityTestLinkedIdentity';

const testLinkedLibraryId = 'record_identity_test_linked_library';
const testLinkAttributeId = 'record_identity_test_link_attribute';
let recordIdLinkIdentity;
let recordIdInLinkedLibrary;

// For identity through tree attribute
const testTreeIdentityLibraryId = 'record_library_test_tree_attribute';
const testTreeIdentityLibraryTypeName = 'recordLibraryTestTreeAttribute';

const testTreeId = 'record_identity_test_tree';
const testTreeRecordLibraryId = 'record_identity_test_tree_record_library';
const testTreeAttributeId = 'record_identity_test_tree_attribute';
let recordIdTreeIdentity;
let recordIdInTree;

// Identity attributes
const testLabelAttributeId = 'record_identity_test_label_attribute';
const testColorAttributeId = 'record_identity_test_color_attribute';

beforeAll(async () => {
// Create base library
await gqlSaveLibrary(testLibraryId, 'Test Lib');

// Create color attribute
await gqlSaveAttribute({
id: testColorAttributeId,
label: 'Test attribute',
type: AttributeTypes.SIMPLE,
format: AttributeFormats.TEXT
});

// Create label attribute
await gqlSaveAttribute({
id: testLabelAttributeId,
label: 'Test attribute',
type: AttributeTypes.SIMPLE,
format: AttributeFormats.TEXT
});

await gqlSaveAttribute({
id: testLinkAttributeId,
label: 'Test attribute',
type: AttributeTypes.ADVANCED_LINK,
linkedLibrary: testLinkedLibraryId
});

await makeGraphQlCall(
`mutation {
saveLibrary(library: {
id: "${testLinkedLibraryId}",
label: {fr: "Test Lib"},
attributes: ["${testLabelAttributeId}", "${testColorAttributeId}"]
recordIdentityConf: {
label: "${testLabelAttributeId}",
color: "${testColorAttributeId}",
}
}) { id }
}`,
true
);

await makeGraphQlCall(
`mutation {
saveLibrary(library: {
id: "${testTreeRecordLibraryId}",
label: {fr: "Test Lib"},
attributes: ["${testLabelAttributeId}", "${testColorAttributeId}"]
recordIdentityConf: {
label: "${testLabelAttributeId}",
color: "${testColorAttributeId}",
}
}) { id }
}`,
true
);

await makeGraphQlCall(
`mutation {
saveLibrary(library: {
id: "${testLinkedIdentityLibraryId}",
label: {fr: "Test Lib"},
attributes: ["${testLinkAttributeId}"]
recordIdentityConf: {
label: "${testLinkAttributeId}",
color: "${testLinkAttributeId}",
}
}) { id }
}`,
true
);

await gqlSaveLibrary(testLibraryId, 'Test Lib', [testLinkAttributeId]);

await gqlSaveTree(testTreeId, 'Test tree', [testTreeRecordLibraryId]);
await gqlSaveAttribute({
id: testTreeAttributeId,
label: 'Test attribute',
linkedTree: testTreeId,
type: AttributeTypes.TREE
});

await makeGraphQlCall(
`mutation {
saveLibrary(library: {
id: "${testTreeIdentityLibraryId}",
label: {fr: "Test Lib"},
attributes: ["${testTreeAttributeId}"],
recordIdentityConf: {
label: "${testTreeAttributeId}",
color: "${testTreeAttributeId}",
}
}) { id }
}`,
true
);

await makeGraphQlCall('mutation { refreshSchema }');

const resCrea = await makeGraphQlCall(`mutation {
c1: createRecord(library: "${testLibraryId}") { id }
c2: createRecord(library: "${testLinkedIdentityLibraryId}") { id }
c3: createRecord(library: "${testTreeIdentityLibraryId}") { id }
c4: createRecord(library: "${testLinkedLibraryId}") { id }
c5: createRecord(library: "${testTreeRecordLibraryId}") { id }
}`);

recordId = resCrea.data.data.c1.id;
recordIdLinkIdentity = resCrea.data.data.c2.id;
recordIdTreeIdentity = resCrea.data.data.c3.id;
recordIdInLinkedLibrary = resCrea.data.data.c4.id;
recordIdInTree = resCrea.data.data.c5.id;

// Values for linked identity
await gqlSaveValue(
testLinkAttributeId,
testLinkedIdentityLibraryId,
recordIdLinkIdentity,
recordIdInLinkedLibrary
);
await gqlSaveValue(testLabelAttributeId, testLinkedLibraryId, recordIdInLinkedLibrary, 'my linked label');
await gqlSaveValue(testColorAttributeId, testLinkedLibraryId, recordIdInLinkedLibrary, '#123456');

// Values for tree identity
const nodeId = await gqlAddElemToTree(testTreeId, {id: recordIdInTree, library: testTreeRecordLibraryId});
await gqlSaveValue(testTreeAttributeId, testTreeIdentityLibraryId, recordIdTreeIdentity, nodeId);
await gqlSaveValue(testLabelAttributeId, testTreeRecordLibraryId, recordIdInTree, 'my tree label');
await gqlSaveValue(testColorAttributeId, testTreeRecordLibraryId, recordIdInTree, '#654321');
});

test('Retrieve record identity', async () => {
const res = await makeGraphQlCall(`
{
${testLibraryTypeName}(filters: [{field: "id", condition: ${AttributeCondition.EQUAL}, value: "${recordId}"}]) {
list {
id
whoAmI { id library { id } label }
}
}
}
`);

expect(res.data.errors).toBeUndefined();
expect(res.status).toBe(200);
expect(res.data.data[testLibraryTypeName].list[0].whoAmI.id).toBe(recordId);
expect(res.data.data[testLibraryTypeName].list[0].whoAmI.library.id).toBe(testLibraryId);
expect(res.data.data[testLibraryTypeName].list[0].whoAmI.label).toBe(null);
});

test('Retrieve label based on link attribute', async () => {
const res = await makeGraphQlCall(`
{
${testLinkedIdentityLibraryTypeName}(
filters: [
{field: "id", condition: ${AttributeCondition.EQUAL}, value: "${recordIdLinkIdentity}"}
]
) {
list {
id
whoAmI { id library { id } label color }
}
}
}
`);

expect(res.data.errors).toBeUndefined();
expect(res.status).toBe(200);
expect(res.data.data[testLinkedIdentityLibraryTypeName].list[0].whoAmI.label).toBe('my linked label');
expect(res.data.data[testLinkedIdentityLibraryTypeName].list[0].whoAmI.color).toBe('#123456');
});

test('Retrieve label based on tree attribute', async () => {
const res = await makeGraphQlCall(`
{
${testTreeIdentityLibraryTypeName}(filters: [
{field: "id", condition: ${AttributeCondition.EQUAL}, value: "${recordIdTreeIdentity}"}
]
) {
list {
id
whoAmI { id library { id } label color}
}
}
}
`);

expect(res.data.errors).toBeUndefined();
expect(res.status).toBe(200);
expect(res.data.data[testTreeIdentityLibraryTypeName].list[0].whoAmI.label).toBe('my tree label');
expect(res.data.data[testTreeIdentityLibraryTypeName].list[0].whoAmI.color).toBe('#654321');
});
});
24 changes: 2 additions & 22 deletions apps/core/src/__tests__/e2e/api/records/records.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ import {
describe('Records', () => {
const testLibName = 'record_library_test';
const testLibNameType = 'recordLibraryTest';

let recordId;

beforeAll(async () => {
await makeGraphQlCall(`mutation {
saveLibrary(library: {id: "${testLibName}", label: {fr: "Test lib"}}) { id }
}`);
await gqlSaveLibrary(testLibName, 'Test Lib');

await makeGraphQlCall('mutation { refreshSchema }');

Expand Down Expand Up @@ -81,25 +80,6 @@ describe('Records', () => {
expect(res.data.data[testLibNameType].list[0].library.id).toBe(testLibName);
});

test('Get record identity', async () => {
const res = await makeGraphQlCall(`
{
${testLibNameType}(filters: [{field: "id", condition: ${AttributeCondition.EQUAL}, value: "${recordId}"}]) {
list {
id
whoAmI { id library { id } label }
}
}
}
`);

expect(res.data.errors).toBeUndefined();
expect(res.status).toBe(200);
expect(res.data.data[testLibNameType].list[0].whoAmI.id).toBe(recordId);
expect(res.data.data[testLibNameType].list[0].whoAmI.library.id).toBe(testLibName);
expect(res.data.data[testLibNameType].list[0].whoAmI.label).toBe(null);
});

test('Get records paginated', async () => {
const firstCallRes = await makeGraphQlCall(
`{ ${testLibNameType}(pagination: {limit: 3, offset: 0}) { totalCount cursor {next prev} list {id} } }
Expand Down
30 changes: 22 additions & 8 deletions apps/core/src/domain/record/helpers/getAttributesFromField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,27 @@ export interface IGetAttributesFromFieldsHelper {
* @param field
* @param ctx
*/
const getAttributesFromField = async (
field: string,
condition: IRecordFilterLight['condition'],
deps: IDeps,
ctx: IQueryInfos
): Promise<IAttribute[]> => {
const getAttributesFromField = async (params: {
field: string;
condition: IRecordFilterLight['condition'];
visitedLibraries?: string[];
deps: IDeps;
ctx: IQueryInfos;
}): Promise<IAttribute[]> => {
const {field, condition, visitedLibraries = [], deps, ctx} = params;
const {
'core.domain.attribute': attributeDomain = null,
'core.infra.library': libraryRepo = null,
'core.infra.tree': treeRepo = null
} = deps;

const _getLabelOrIdAttribute = async (library: string): Promise<string> => {
if (visitedLibraries.includes(library)) {
return 'id';
}

visitedLibraries.push(library);

const linkedLibraryProps = await libraryRepo.getLibraries({
params: {filters: {id: library}},
ctx
Expand Down Expand Up @@ -101,7 +109,13 @@ const getAttributesFromField = async (
// For example, if we filter on "category.created_by", we'll actually search on category.created_by.label
const subChildAttributes =
condition !== AttributeCondition.IS_EMPTY && condition !== AttributeCondition.IS_NOT_EMPTY
? await getAttributesFromField(childAttribute, condition, deps, ctx)
? await getAttributesFromField({
field: childAttribute,
visitedLibraries,
condition,
deps,
ctx
})
: [];
attributes = [...attributes, ...subChildAttributes];

Expand Down Expand Up @@ -180,7 +194,7 @@ const getAttributesFromField = async (

// Calling this function recursively will handle the case where child attribute is a link
// For example, if we filter on "category.created_by", we'll actually search on category.created_by.label
const subChildAttributes = await getAttributesFromField(childAttribute, condition, deps, ctx);
const subChildAttributes = await getAttributesFromField({field: childAttribute, condition, deps, ctx});
attributes = [...attributes, ...subChildAttributes];
}

Expand Down
16 changes: 14 additions & 2 deletions apps/core/src/domain/record/recordDomain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {ActionsListEvents} from '../../_types/actionsList';
import {AttributeFormats, AttributeTypes} from '../../_types/attribute';
import {AttributeCondition, IRecord, Operator} from '../../_types/record';
import {mockAttrAdvLink, mockAttrSimple, mockAttrSimpleLink, mockAttrTree} from '../../__tests__/mocks/attribute';
import {mockLibrary} from '../../__tests__/mocks/library';
import {mockLibrary, mockLibraryFiles} from '../../__tests__/mocks/library';
import {mockRecord} from '../../__tests__/mocks/record';
import {mockCtx} from '../../__tests__/mocks/shared';
import {mockTree} from '../../__tests__/mocks/tree';
Expand Down Expand Up @@ -801,15 +801,27 @@ describe('RecordDomain', () => {
])
};

const mockLibraryRepo: Mockify<ILibraryRepo> = {
getLibraries: global.__mockPromise({totalCount: 1, list: [mockLibraryFiles]})
};

const mockAttributeDomain: Mockify<IAttributeDomain> = {
getAttributeProperties: global.__mockPromise(mockAttrSimple)
};

const mockGetEntityByIdHelper = jest.fn().mockReturnValue(libData);

const mockUtils: Mockify<IUtils> = {
getPreviewsAttributeName: jest.fn().mockReturnValue('previews')
getPreviewsAttributeName: jest.fn().mockReturnValue('previews'),
isLinkAttribute: jest.fn().mockReturnValue(false),
isTreeAttribute: jest.fn().mockReturnValue(false)
};

const recDomain = recordDomain({
'core.domain.value': mockValDomain as IValueDomain,
'core.domain.attribute': mockAttributeDomain as IAttributeDomain,
'core.domain.helpers.getCoreEntityById': mockGetEntityByIdHelper,
'core.infra.library': mockLibraryRepo as ILibraryRepo,
'core.utils': mockUtils as IUtils,
config: mockConfig as Config.IConfig
});
Expand Down
Loading
Loading