From 7785b71d57d5332350838f262407312718c899a5 Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Thu, 27 Mar 2025 16:28:40 +0000 Subject: [PATCH] Normalise create and update actions Create and update actions are separated into different properties to ensure distinction without relying on shaky conditional logic. --- src/definition/index.js | 3 + src/definition/normalise-actions.js | 80 ++++++++ src/definition/normalise-actions.test.js | 242 +++++++++++++++++++++++ src/definition/normalise-type.js | 6 +- src/definition/normalise-type.test.js | 111 ++++++++++- 5 files changed, 435 insertions(+), 7 deletions(-) create mode 100644 src/definition/normalise-actions.js create mode 100644 src/definition/normalise-actions.test.js diff --git a/src/definition/index.js b/src/definition/index.js index ba15265..59a0ffe 100644 --- a/src/definition/index.js +++ b/src/definition/index.js @@ -1,6 +1,7 @@ import extractCollectionMember from './extract-collection-member.js'; import extractField from './extract-field.js'; import extractMember from './extract-member.js'; +import {normaliseCreateActions, normaliseUpdateActions} from './normalise-actions.js'; import normaliseActionLayout from './normalise-action-layout.js'; import normaliseFields from './normalise-fields.js'; import {normaliseSearchInputsLayout, normaliseSearchResultsLayout} from './normalise-search-layout.js'; @@ -13,10 +14,12 @@ export { extractField, extractMember, normaliseActionLayout, + normaliseCreateActions, normaliseFields, normaliseSearchInputsLayout, normaliseSearchResultsLayout, normaliseStates, normaliseType, + normaliseUpdateActions, normaliseViewLayout, }; diff --git a/src/definition/normalise-actions.js b/src/definition/normalise-actions.js new file mode 100644 index 0000000..6ff1afa --- /dev/null +++ b/src/definition/normalise-actions.js @@ -0,0 +1,80 @@ +import * as AclV2 from '../acl-v2.js'; + +const FROM_STATE_WILDCARD = '*'; + +export const normaliseCreateActions = (events) => Object.fromEntries( + events.filter(isCreateAction) + .map((event) => [event.id, normaliseCreate(event)]) +); + +export const normaliseUpdateActions = (events) => Object.fromEntries( + events.filter(isUpdateAction) + .map((event) => [event.id, normaliseUpdate(event)]) +); + +const isCreateAction = (event) => !event.pre_states?.length; + +const isUpdateAction = (event) => event.pre_states?.length > 0; + +const normaliseCreate = (event) => ({ + id: event.id, + name: event.name, + description: orUndefined(event.description), + order: event.order, + toState: event.post_state, + postconditions: normalisePostconditions(event.postconditions), + acl: AclV2.fromLegacy(event.acls), + classification: event.security_classification, + webhooks: { + onStart: normaliseWebhook(event.callback_url_about_to_start_event, event.retries_timeout_about_to_start_event), + onSubmit: normaliseWebhook(event.callback_url_about_to_submit_event, event.retries_timeout_url_about_to_submit_event), + onSubmitted: normaliseWebhook(event.callback_url_submitted_event, event.retries_timeout_url_submitted_event), + }, +}); + +const normaliseUpdate = (event) => ({ + id: event.id, + name: event.name, + description: orUndefined(event.description), + order: event.order, + fromStates: normaliseFromStates(event.pre_states), + precondition: orUndefined(event.precondition), + toState: orUndefined(event.post_state), + postconditions: normalisePostconditions(event.postconditions), + acl: AclV2.fromLegacy(event.acls), + classification: event.security_classification, + webhooks: { + onStart: normaliseWebhook(event.callback_url_about_to_start_event, event.retries_timeout_about_to_start_event), + onSubmit: normaliseWebhook(event.callback_url_about_to_submit_event, event.retries_timeout_url_about_to_submit_event), + onSubmitted: normaliseWebhook(event.callback_url_submitted_event, event.retries_timeout_url_submitted_event), + }, +}); + +const orUndefined = (value) => value ? value : undefined; + +const normaliseFromStates = (fromStates) => { + if (fromStates.includes(FROM_STATE_WILDCARD)) return; + + return fromStates; +} + +/** + * Preserve postconditions as an ordered array in case postconditions application order becomes important in the future. + */ +const normalisePostconditions = (postconditions) => { + if (!postconditions?.length) return; + + return postconditions.map(({field_id, value}) => ({ + path: field_id, + value, + })); +}; + +const normaliseWebhook = (url, retries) => { + if (!url) return; + + return { + url, + retries, + }; +}; diff --git a/src/definition/normalise-actions.test.js b/src/definition/normalise-actions.test.js new file mode 100644 index 0000000..b4aaa74 --- /dev/null +++ b/src/definition/normalise-actions.test.js @@ -0,0 +1,242 @@ +import * as AclV2 from '../acl-v2.js'; +import {normaliseCreateActions, normaliseUpdateActions} from './normalise-actions.js'; + +const CR = AclV2.CREATE | AclV2.READ; + +describe('normaliseCreateActions', () => { + const actions = [ + { + id: 'action1', + name: 'Create action 1', + description: 'A first create action', + order: 1, + pre_states: [], + post_state: 'state1', + acls: [ + { + role: 'caseworker-test-write', + create: true, + read: true, + }, + { + role: 'caseworker-test-read', + create: false, + read: true, + } + ], + security_classification: 'PUBLIC', + }, + { + id: 'action2', + name: 'Create action 2', + description: null, + order: 2, + pre_states: [], + post_state: 'state2', + postconditions: [ + {field_id: 'field1', value: 'value1'}, + {field_id: 'field2', value: 'value2'}, + ], + acls: [ + { + role: 'caseworker-test-write', + create: true, + read: false, + } + ], + security_classification: 'PRIVATE', + callback_url_about_to_start_event: 'http://webhooks/start', + retries_timeout_about_to_start_event: [1,2,3], + callback_url_about_to_submit_event: 'http://webhooks/submit', + retries_timeout_url_about_to_submit_event: [4,5,6], + callback_url_submitted_event: 'http://webhooks/submitted', + retries_timeout_url_submitted_event: [7,8,9], + }, + ]; + + test('should return empty object when no actions', () => { + expect(normaliseCreateActions([])).toEqual({}); + }); + + test('should transform array of events into object of normalised create actions', () => { + expect(normaliseCreateActions(actions)).toEqual({ + 'action1': { + id: 'action1', + name: 'Create action 1', + description: 'A first create action', + order: 1, + toState: 'state1', + acl: { + 'caseworker-test-write': CR, + 'caseworker-test-read': AclV2.READ, + }, + classification: 'PUBLIC', + webhooks: {}, + }, + 'action2': { + id: 'action2', + name: 'Create action 2', + order: 2, + toState: 'state2', + postconditions: [ + {path: 'field1', value: 'value1'}, + {path: 'field2', value: 'value2'}, + ], + acl: { + 'caseworker-test-write': AclV2.CREATE, + }, + classification: 'PRIVATE', + webhooks: { + onStart: { + url: 'http://webhooks/start', + retries: [1,2,3] + }, + onSubmit: { + url: 'http://webhooks/submit', + retries: [4,5,6] + }, + onSubmitted: { + url: 'http://webhooks/submitted', + retries: [7,8,9] + }, + }, + }, + }); + }); + + test('should filter out update actions', () => { + const createActions = normaliseCreateActions([ + ...actions, + { + id: 'updateAction1', + name: 'Update action', + pre_states: ['state1'], + acls: [], + } + ]); + + expect(createActions).not.toHaveProperty('updateAction1'); + }); +}); + +describe('normaliseUpdateActions', () => { + const actions = [ + { + id: 'action1', + name: 'Update action 1', + description: 'A first update action', + order: 1, + pre_states: ['*'], + precondition: 'field1 EQUALS "value1"', + post_state: 'state1', + postconditions: [], + acls: [ + { + role: 'caseworker-test-write', + create: true, + read: true, + }, + { + role: 'caseworker-test-read', + create: false, + read: true, + } + ], + security_classification: 'PUBLIC', + }, + { + id: 'action2', + name: 'Update action 2', + description: null, + order: 2, + pre_states: ['state1', 'state3'], + precondition: null, + post_state: 'state2', + postconditions: [ + {field_id: 'field1', value: 'value1'}, + {field_id: 'field2', value: 'value2'}, + ], + acls: [ + { + role: 'caseworker-test-write', + create: true, + read: false, + } + ], + security_classification: 'PRIVATE', + callback_url_about_to_start_event: 'http://webhooks/start', + retries_timeout_about_to_start_event: [1,2,3], + callback_url_about_to_submit_event: 'http://webhooks/submit', + retries_timeout_url_about_to_submit_event: [4,5,6], + callback_url_submitted_event: 'http://webhooks/submitted', + retries_timeout_url_submitted_event: [7,8,9], + }, + ]; + + test('should return empty object when no actions', () => { + expect(normaliseUpdateActions([])).toEqual({}); + }); + + test('should transform array of events into object of normalised update actions', () => { + expect(normaliseUpdateActions(actions)).toEqual({ + 'action1': { + id: 'action1', + name: 'Update action 1', + description: 'A first update action', + order: 1, + fromStates: undefined, + precondition: 'field1 EQUALS "value1"', + toState: 'state1', + acl: { + 'caseworker-test-write': CR, + 'caseworker-test-read': AclV2.READ, + }, + classification: 'PUBLIC', + webhooks: {}, + }, + 'action2': { + id: 'action2', + name: 'Update action 2', + order: 2, + fromStates: ['state1', 'state3'], + toState: 'state2', + postconditions: [ + {path: 'field1', value: 'value1'}, + {path: 'field2', value: 'value2'}, + ], + acl: { + 'caseworker-test-write': AclV2.CREATE, + }, + classification: 'PRIVATE', + webhooks: { + onStart: { + url: 'http://webhooks/start', + retries: [1,2,3] + }, + onSubmit: { + url: 'http://webhooks/submit', + retries: [4,5,6] + }, + onSubmitted: { + url: 'http://webhooks/submitted', + retries: [7,8,9] + }, + }, + }, + }); + }); + + test('should filter out update actions', () => { + const createActions = normaliseUpdateActions([ + ...actions, + { + id: 'createAction1', + name: 'Create action', + pre_states: [], + acls: [], + } + ]); + + expect(createActions).not.toHaveProperty('createAction1'); + }); +}); diff --git a/src/definition/normalise-type.js b/src/definition/normalise-type.js index 6197ca1..bae8b61 100644 --- a/src/definition/normalise-type.js +++ b/src/definition/normalise-type.js @@ -1,4 +1,5 @@ import * as AclV2 from '../acl-v2.js'; +import {normaliseCreateActions, normaliseUpdateActions} from './normalise-actions.js'; import normaliseFields from './normalise-fields.js'; import normaliseStates from './normalise-states.js'; @@ -18,9 +19,10 @@ const normaliseType = (type) => ({ description: type.description, title: type.titleDisplay, acl: AclV2.fromLegacy(type.acls), - events: {}, // TODO once normalisation of events implemented fields: normaliseFields(type.case_fields), + createActions: normaliseCreateActions(type.events), + actions: normaliseUpdateActions(type.events), states: normaliseStates(type.states), }); -export default normaliseType; \ No newline at end of file +export default normaliseType; diff --git a/src/definition/normalise-type.test.js b/src/definition/normalise-type.test.js index 4407916..a60d153 100644 --- a/src/definition/normalise-type.test.js +++ b/src/definition/normalise-type.test.js @@ -1,4 +1,4 @@ -import {CRUD, READ} from '../acl-v2.js'; +import {CREATE, CRUD, READ} from '../acl-v2.js'; import normaliseType from './normalise-type.js'; test('should normalise simple type with no events/fields/states', () => { @@ -17,7 +17,8 @@ test('should normalise simple type with no events/fields/states', () => { name: 'Type 1', description: 'Description for type 1', acl: {}, - events: {}, + createActions: {}, + actions: {}, fields: {}, states: {}, }); @@ -55,7 +56,8 @@ test('should normalise ACLs', () => { 'role-1': CRUD, 'role-2': READ, }, - events: {}, + createActions: {}, + actions: {}, fields: {}, states: {}, }); @@ -97,7 +99,8 @@ test('should normalise fields', () => { id: 'type1', name: 'Type 1', acl: {}, - events: {}, + createActions: {}, + actions: {}, fields: { 'textField1': { id: 'textField1', @@ -114,6 +117,103 @@ test('should normalise fields', () => { }); }); +test('should normalise create actions', () => { + const rawType = { + id: 'type1', + name: 'Type 1', + acls: [], + events: [ + { + id: 'create', + name: 'Create', + description: 'Create new instance', + order: 1, + pre_states: [], + post_state: 'state1', + acls: [ + { + role: 'role-1', + create: true, + read: true, + } + ] + } + ], + case_fields: [], + states: [], + }; + + expect(normaliseType(rawType)).toEqual({ + id: 'type1', + name: 'Type 1', + acl: {}, + createActions: { + 'create': { + id: 'create', + name: 'Create', + description: 'Create new instance', + order: 1, + toState: 'state1', + acl: { + 'role-1': CREATE | READ, + }, + webhooks: {}, + } + }, + actions: {}, + fields: {}, + states: {}, + }); +}); + +test('should normalise update actions', () => { + const rawType = { + id: 'type1', + name: 'Type 1', + acls: [], + events: [ + { + id: 'update', + name: 'Update', + description: 'Update record', + order: 1, + pre_states: ['*'], + post_state: null, + acls: [ + { + role: 'role-1', + create: true, + read: true, + } + ] + } + ], + case_fields: [], + states: [], + }; + + expect(normaliseType(rawType)).toEqual({ + id: 'type1', + name: 'Type 1', + acl: {}, + createActions: {}, + actions: { + 'update': { + id: 'update', + name: 'Update', + description: 'Update record', + order: 1, + acl: { + 'role-1': CREATE | READ, + }, + webhooks: {}, + } + }, + fields: {}, + states: {}, + }); +}); + test('should normalise states', () => { const rawType = { id: 'type1', @@ -145,7 +245,8 @@ test('should normalise states', () => { id: 'type1', name: 'Type 1', acl: {}, - events: {}, + createActions: {}, + actions: {}, fields: {}, states: { 'created': {