Skip to content

Commit

Permalink
feat: add a query to run actions list and format on a given value and…
Browse files Browse the repository at this point in the history
… use it in @leav/ui
  • Loading branch information
philippechevieux committed Nov 12, 2024
1 parent d88c7ae commit dd8d51c
Show file tree
Hide file tree
Showing 10 changed files with 452 additions and 34 deletions.
37 changes: 37 additions & 0 deletions apps/core/src/__tests__/e2e/api/values/values.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('Values', () => {
const treeLibName = 'tree_library_test';

const attrSimpleName = 'values_attribute_test_simple';
const attrSimpleNameWithFormat = 'values_attribute_test_simple_with_format';
const attrSimpleExtendedName = 'values_attribute_test_simple_extended';
const attrSimpleLinkName = 'values_attribute_test_simple_link';
const attrAdvancedName = 'values_attribute_test_adv';
Expand Down Expand Up @@ -50,6 +51,16 @@ describe('Values', () => {
}
}`);

await gqlSaveAttribute({
id: attrSimpleNameWithFormat,
type: AttributeTypes.SIMPLE,
format: AttributeFormats.TEXT,
label: 'Test attr simple with format',
actionsList: {
getValue: [{id: 'toUppercase', name: 'toUppercase'}]
}
});

await makeGraphQlCall(`mutation {
saveAttribute(
attribute: {
Expand Down Expand Up @@ -184,6 +195,7 @@ describe('Values', () => {
"created_by",
"created_at",
"${attrSimpleName}",
"${attrSimpleNameWithFormat}",
"${attrAdvancedName}",
"${attrSimpleLinkName}",
"${attrAdvancedLinkName}",
Expand Down Expand Up @@ -625,4 +637,29 @@ describe('Values', () => {
expect(res.data.errors[0].extensions.fields[attrDateRangeName]).toBeDefined();
});
});

test('Run actions list and format on value', async () => {
const res = await makeGraphQlCall(`query {
runActionsListAndFormatOnValue(
library: "${testLibName}",
value: {
attribute: "${attrSimpleNameWithFormat}",
payload: "test",
metadata: null
},
version: null
) {
id_value
payload
raw_payload
}
}`);

expect(res.status).toBe(200);

expect(res.data.errors).toBeUndefined();
expect(res.data.data.runActionsListAndFormatOnValue[0].id_value).toBeNull();
expect(res.data.data.runActionsListAndFormatOnValue[0].payload).toBe('TEST');
expect(res.data.data.runActionsListAndFormatOnValue[0].raw_payload).toBe('test');
});
});
11 changes: 11 additions & 0 deletions apps/core/src/_types/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import {IDbEdge} from 'infra/db/_types';
import {IRecord} from './record';
import {ITreeNode, TreePaths} from './tree';

export type IValueFromGql = Override<
Omit<IValue, 'version'>,
{
value: IValue['payload'];
metadata: Array<{
name: string;
value: string;
}>;
}
>;

export interface IValueVersion {
[treeName: string]: string;
}
Expand Down
31 changes: 29 additions & 2 deletions apps/core/src/app/core/valueApp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06
// This file is released under LGPL V3
// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt
import {IKeyValue, objectToNameValueArray} from '@leav/utils';
import {IKeyValue, objectToNameValueArray, Override} from '@leav/utils';
import {ConvertVersionFromGqlFormatFunc} from 'app/helpers/convertVersionFromGqlFormat';
import {IAttributeDomain} from 'domain/attribute/attributeDomain';
import {IRecordDomain} from 'domain/record/recordDomain';
Expand All @@ -10,7 +10,7 @@ import {IUtils} from 'utils/utils';
import {IAppGraphQLSchema} from '_types/graphql';
import {IQueryInfos} from '_types/queryInfos';
import {IRecord} from '_types/record';
import {IStandardValue, ITreeValue, IValue, IValueVersion} from '_types/value';
import {IStandardValue, ITreeValue, IValue, IValueFromGql, IValueVersion, IValueVersionFromGql} from '_types/value';
import {AttributeTypes, IAttribute} from '../../_types/attribute';
import {AttributeCondition} from '../../_types/record';

Expand Down Expand Up @@ -232,6 +232,10 @@ export default function ({
deleteValue(library: ID!, recordId: ID!, attribute: ID!, value: ValueInput): [GenericValue!]!
}
extend type Query {
runActionsListAndFormatOnValue(library: ID, value: ValueBatchInput, version: [ValueVersionInput]): [Value!]!
}
`,
resolvers: {
Mutation: {
Expand Down Expand Up @@ -300,6 +304,29 @@ export default function ({
});
}
},
Query: {
async runActionsListAndFormatOnValue(
_: never,
{
library,
value,
version
}: {library: string; value: IValueFromGql; version: IValueVersionFromGql},
ctx: IQueryInfos
): Promise<IValue[]> {
const convertedValue = {
...value,
version: convertVersionFromGqlFormat(version),
metadata: utils.nameValArrayToObj(value.metadata)
};

return valueDomain.runActionsListAndFormatOnValue({
library,
value: convertedValue,
ctx
});
}
},
GenericValue: {
__resolveType: async (fieldValue, _, ctx) => {
const attribute = Array.isArray(fieldValue)
Expand Down
111 changes: 111 additions & 0 deletions apps/core/src/domain/value/valueDomain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2368,4 +2368,115 @@ describe('ValueDomain', () => {
await expect(getVal).rejects.toHaveProperty('fields.recordId');
});
});

describe('runActionsListAndFormatOnValue', () => {
test('Should return the same value if no actions list', async function () {
const payload = 'test val';

const mockAttrDomain: Mockify<IAttributeDomain> = {
getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE})
};

const valDomain = valueDomain({
...depsBase,
'core.domain.attribute': mockAttrDomain as IAttributeDomain,
'core.infra.record': mockRecordRepo as IRecordRepo,
'core.domain.actionsList': mockActionsListDomain as any,
'core.domain.helpers.validate': mockValidateHelper as IValidateHelper,
'core.utils': mockUtilsStandardAttribute as IUtils
});

const resValue = await valDomain.runActionsListAndFormatOnValue({
library: 'test_lib',
value: {
attribute: 'test_attr',
payload
},
ctx
});
expect(resValue).toMatchObject([{payload, raw_payload: payload, attribute: 'test_attr'}]);
});

test('Should return formatted value after actions list', async function () {
const payload = 'test val';
const formattedPayload = 'TEST VAL';

const mockAttrDomain: Mockify<IAttributeDomain> = {
getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE})
};

const mockActionsListDomainUpperCase: Mockify<IActionsListDomain> = {
runActionsList: jest
.fn()
.mockImplementation((_, val) => Promise.resolve([{...val[0], payload: formattedPayload}]))
};

const valDomain = valueDomain({
...depsBase,
'core.domain.attribute': mockAttrDomain as IAttributeDomain,
'core.infra.record': mockRecordRepo as IRecordRepo,
'core.domain.actionsList': mockActionsListDomainUpperCase as any,
'core.domain.helpers.validate': mockValidateHelper as IValidateHelper,
'core.utils': mockUtilsStandardAttribute as IUtils
});

const resValue = await valDomain.runActionsListAndFormatOnValue({
library: 'test_lib',
value: {
attribute: 'test_attr',
payload
},
ctx
});
expect(resValue).toMatchObject([{payload: formattedPayload, raw_payload: payload, attribute: 'test_attr'}]);
});

test('Should return fomatted value after actions list if attribute format is date range', async function () {
const dateRangePayload = JSON.stringify({from: '1727733600', to: '1729548000'});
const dateRangeFormattedPayload = {
from: 'Monday, Septembre 30, 2024',
to: 'Tuesday, October 1, 2024'
};

const mockAttrDomain: Mockify<IAttributeDomain> = {
getAttributeProperties: global.__mockPromise({...mockAttribute, type: AttributeTypes.SIMPLE})
};

const mockActionsListDomainDateRange: Mockify<IActionsListDomain> = {
runActionsList: jest.fn().mockImplementation((_, val) =>
Promise.resolve([
{
...val[0],
payload: dateRangeFormattedPayload
}
])
)
};

const valDomain = valueDomain({
...depsBase,
'core.domain.attribute': mockAttrDomain as IAttributeDomain,
'core.infra.record': mockRecordRepo as IRecordRepo,
'core.domain.actionsList': mockActionsListDomainDateRange as any,
'core.domain.helpers.validate': mockValidateHelper as IValidateHelper,
'core.utils': mockUtilsStandardAttribute as IUtils
});

const resValue = await valDomain.runActionsListAndFormatOnValue({
library: 'test_lib',
value: {
attribute: 'test_attr',
payload: dateRangePayload
},
ctx
});
expect(resValue).toMatchObject([
{
payload: dateRangeFormattedPayload,
raw_payload: dateRangePayload,
attribute: 'test_attr'
}
]);
});
});
});
90 changes: 62 additions & 28 deletions apps/core/src/domain/value/valueDomain.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06
// This file is released under LGPL V3
// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt
import {EventAction} from '@leav/utils';
import {EventAction, IDateRangeValue} from '@leav/utils';
import {IEventsManagerDomain} from 'domain/eventsManager/eventsManagerDomain';
import {UpdateRecordLastModifFunc} from 'domain/helpers/updateRecordLastModif';
import {SendRecordUpdateEventHelper} from 'domain/record/helpers/sendRecordUpdateEvent';
Expand All @@ -19,7 +19,7 @@ import {IRecord} from '_types/record';
import PermissionError from '../../errors/PermissionError';
import ValidationError from '../../errors/ValidationError';
import {ActionsListEvents} from '../../_types/actionsList';
import {AttributeTypes, IAttribute, ValueVersionMode} from '../../_types/attribute';
import {AttributeFormats, AttributeTypes, IAttribute, ValueVersionMode} from '../../_types/attribute';
import {Errors, ErrorTypes} from '../../_types/errors';
import {RecordAttributePermissionsActions, RecordPermissionsActions} from '../../_types/permissions';
import {IQueryInfos} from '../../_types/queryInfos';
Expand Down Expand Up @@ -117,6 +117,16 @@ export interface IValueDomain {
}): Promise<IValue>;

runActionsList(params: IRunActionListParams): Promise<IValue[]>;

runActionsListAndFormatOnValue({
library,
value,
ctx
}: {
library: string;
value: IValue;
ctx: IQueryInfos;
}): Promise<IValue[]>;
}

export interface IValueDomainDeps {
Expand Down Expand Up @@ -216,10 +226,7 @@ const valueDomain = function ({
}): Promise<IValue> => {
let processedValue = {...value}; // Don't mutate given value

const isLinkAttribute =
attribute.type === AttributeTypes.SIMPLE_LINK || attribute.type === AttributeTypes.ADVANCED_LINK;

if (isLinkAttribute && attribute.linked_library) {
if (utils.isLinkAttribute(attribute)) {
const linkValue = processedValue.payload
? {...processedValue.payload, library: processedValue.payload.library ?? attribute.linked_library}
: null;
Expand Down Expand Up @@ -452,10 +459,40 @@ const valueDomain = function ({
);
}

// Apply actions list and format value before returning it
const processedValues = await _runActionsListAndFormatValue(library, attribute, savedValue, ctx, record);

if (!areValuesIdentical) {
await eventsManager.sendDatabaseEvent(
{
action: EventAction.VALUE_SAVE,
topic: {
library,
record: {
id: record.id,
libraryId: library
},
attribute: attribute.id
},
before: valueBefore,
after: savedValue
},
ctx
);
}

return {values: processedValues, areValuesIdentical};
};

const _runActionsListAndFormatValue = async (
library: string,
attribute: IAttribute,
value: IValue,
ctx: IQueryInfos,
record: IRecord = null
) => {
let processedValues = await _runActionsList({
listName: ActionsListEvents.GET_VALUE,
values: [savedValue],
values: [value],
attribute,
record,
library,
Expand Down Expand Up @@ -500,26 +537,7 @@ const valueDomain = function ({
})
);

if (!areValuesIdentical) {
await eventsManager.sendDatabaseEvent(
{
action: EventAction.VALUE_SAVE,
topic: {
library,
record: {
id: record.id,
libraryId: library
},
attribute: attribute.id
},
before: valueBefore,
after: savedValue
},
ctx
);
}

return {values: processedValues, areValuesIdentical};
return processedValues;
};

return {
Expand Down Expand Up @@ -861,6 +879,22 @@ const valueDomain = function ({
await validate.validateRecord(library, recordId, ctx);
return _executeDeleteValue({library, recordId, attribute, value, ctx});
},
async runActionsListAndFormatOnValue({library, value, ctx}) {
const attributeProps = await attributeDomain.getAttributeProperties({id: value.attribute, ctx});

if (attributeProps.format === AttributeFormats.DATE_RANGE) {
value.payload = JSON.parse(value.payload) as IDateRangeValue;
}

//TODO: When we will adapt Tree attribute to design-system, we will have to adapt this part (like we did for link)
if (utils.isLinkAttribute(attributeProps)) {
value.payload = {
id: value.payload
};
}

return _runActionsListAndFormatValue(library, attributeProps, value, ctx);
},
formatValue: _formatValue,
runActionsList: _runActionsList
};
Expand Down
Loading

0 comments on commit dd8d51c

Please sign in to comment.