@flowerforce/flower-core 4.0.1-beta.0 → 4.0.1-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## 3.5.2 (2025-04-23)
2
+
3
+
4
+ ### 🩹 Fixes
5
+
6
+ - fix init nodes ([996d8af](https://github.com/flowerforce/flower/commit/996d8af))
7
+
8
+ - package lock ([a3bb210](https://github.com/flowerforce/flower/commit/a3bb210))
9
+
10
+ ## 3.5.1 (2025-04-19)
11
+
12
+
13
+ ### 🩹 Fixes
14
+
15
+ - remove empty object from init state ([8604346](https://github.com/flowerforce/flower/commit/8604346))
16
+
17
+ ## 3.5.0 (2025-04-19)
18
+
19
+
20
+ ### 🚀 Features
21
+
22
+ - remove data blank from init nodes ([#80](https://github.com/flowerforce/flower/pull/80))
23
+
24
+ ## 3.4.0 (2025-04-19)
25
+
26
+
27
+ ### 🚀 Features
28
+
29
+ - added remove value on hide element ([#68](https://github.com/flowerforce/flower/pull/68))
30
+
31
+
32
+ ### 🩹 Fixes
33
+
34
+ - avoid validate hidden field ([#67](https://github.com/flowerforce/flower/pull/67))
35
+
1
36
  ## 3.3.0 (2024-10-08)
2
37
 
3
38
 
package/dist/index.cjs.js CHANGED
@@ -32,7 +32,7 @@ const devtoolState = {};
32
32
  const FlowerStateUtils = {
33
33
  getAllData: (state) => state &&
34
34
  Object.entries(state ?? {}).reduce((acc, [k, v]) => ({ ...acc, [k]: v?.data ?? v }), {}),
35
- selectFlowerFormNode: (name) => (state) => _get(state, name),
35
+ selectFlowerDataNode: (name) => (state) => _get(state, name),
36
36
  makeSelectCurrentNextRules: (name) => (state) => {
37
37
  const nextRules = _get(state, [name, 'nextRules']);
38
38
  const currentNodeId = FlowerStateUtils.makeSelectCurrentNodeId(name)(state);
@@ -44,24 +44,107 @@ const FlowerStateUtils = {
44
44
  return _get(subState, ['current']) || startId;
45
45
  },
46
46
  makeSelectNodeErrors: (name) => (state) => {
47
- const form = FlowerStateUtils.selectFlowerFormNode(name)(state);
48
- return createFormData(form);
47
+ const data = FlowerStateUtils.selectFlowerDataNode(name)(state);
48
+ return generateData(data);
49
49
  }
50
50
  };
51
- const createFormData = (form) => {
52
- const validationErrors = form && form.errors;
51
+ const generateData = (data) => {
52
+ const validationErrors = data && data.errors;
53
53
  const allErrors = Object.values(validationErrors || {});
54
54
  return {
55
- isSubmitted: form?.isSubmitted || false,
56
- isDirty: Object.values(form?.dirty || {}).some(Boolean) || false,
57
- hasFocus: form?.hasFocus,
58
- errors: form?.errors,
59
- customErrors: form?.customErrors,
60
- isValidating: form?.isValidating,
55
+ isSubmitted: data?.isSubmitted || false,
56
+ isDirty: Object.values(data?.dirty || {}).some(Boolean) || false,
57
+ hasFocus: data?.hasFocus,
58
+ errors: data?.errors,
59
+ customErrors: data?.customErrors,
60
+ isValidating: data?.isValidating,
61
61
  isValid: allErrors.flat().length === 0
62
62
  };
63
63
  };
64
64
 
65
+ class AbacEngine {
66
+ constructor(rules = []) {
67
+ // compile string conditions once
68
+ this.rules = (rules || [])
69
+ .map((r) => {
70
+ let conditionFn = undefined;
71
+ if (r.condition) {
72
+ try {
73
+ // NOTE: uses new Function -> ensure rules.json is trusted by the app owner
74
+ conditionFn = new Function('subject', 'resource', 'action', 'environment', `return (${r.condition});`);
75
+ }
76
+ catch (err) {
77
+ console.error('ABAC: failed to compile rule condition', r.id, err);
78
+ }
79
+ }
80
+ return { ...r, conditionFn };
81
+ })
82
+ .sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
83
+ }
84
+ decide(ctx) {
85
+ let anyPermit = false;
86
+ for (const r of this.rules) {
87
+ const matchesAction = Array.isArray(r.action)
88
+ ? r.action.includes(ctx.action)
89
+ : r.action === ctx.action || r.action === '*';
90
+ if (!matchesAction)
91
+ continue;
92
+ let matches = true;
93
+ if (r.conditionFn) {
94
+ try {
95
+ matches = !!r.conditionFn(ctx.subject, ctx.resource, ctx.action, ctx.environment);
96
+ }
97
+ catch (err) {
98
+ matches = false;
99
+ console.error(`ABAC: rule ${r.id} threw while evaluating`, err);
100
+ }
101
+ }
102
+ if (!matches)
103
+ continue;
104
+ if (r.effect === 'Deny') {
105
+ // deny-overrides
106
+ return 'Deny';
107
+ }
108
+ if (r.effect === 'Permit') {
109
+ anyPermit = true;
110
+ }
111
+ }
112
+ return anyPermit ? 'Permit' : 'Deny';
113
+ }
114
+ // utility: checks multiple actions and returns those allowed
115
+ allowedActions(ctxBase, actions) {
116
+ return actions.filter((a) => this.decide({ ...ctxBase, action: a }) === 'Permit');
117
+ }
118
+ }
119
+
120
+ let engine = null;
121
+ let rawRules = null;
122
+ let currentSubject = null;
123
+ function initAbac(rules) {
124
+ engine = new AbacEngine(rules);
125
+ rawRules = rules;
126
+ }
127
+ function getAbacEngine() {
128
+ if (!engine)
129
+ console.warn('ABAC engine non inizializzato');
130
+ return engine;
131
+ }
132
+ function getRawRules() {
133
+ return rawRules;
134
+ }
135
+ function isAbacInitialized() {
136
+ return engine !== null;
137
+ }
138
+ function setSubject(subject) {
139
+ currentSubject = subject;
140
+ }
141
+ function getSubject() {
142
+ return currentSubject;
143
+ }
144
+ function clearSubject() {
145
+ currentSubject = null;
146
+ }
147
+
65
148
  const EMPTY_STRING_REGEXP = /^\s*$/;
66
149
  /**
67
150
  * Defines a utility object named rulesMatcherUtils, which contains various helper functions used for processing rules and data in a rule-matching context.
@@ -85,6 +168,11 @@ const rulesMatcherUtils = {
85
168
  if (!operators[op]) {
86
169
  throw new Error(`Error missing operator:${op}`);
87
170
  }
171
+ if (op === '$can') {
172
+ const subject = getSubject();
173
+ const valid = operators['$can'](subject ?? {}, _get(value, 'action'), _get(value, 'resource'));
174
+ return { valid, name: `${pathWithPrefix}___${name || op}` };
175
+ }
88
176
  const valid = operators[op] && operators[op](valueForKey, valueRef, opt, data);
89
177
  return { valid, name: `${pathWithPrefix}___${name || op}` };
90
178
  },
@@ -242,6 +330,11 @@ const rulesMatcherUtils = {
242
330
  * Defines a set of comparison operators used for matching rules against user input.
243
331
  */
244
332
  const operators = {
333
+ $can: (subject, action, resource) => getAbacEngine()?.decide({
334
+ subject: subject ?? {},
335
+ action,
336
+ resource
337
+ }) === 'Permit',
245
338
  $exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
246
339
  $eq: (a, b) => a === b,
247
340
  $ne: (a, b) => a !== b,
@@ -267,19 +360,19 @@ const operators = {
267
360
  .some((c) => c instanceof RegExp ? c.test(a) : new RegExp(c, opt).test(a))
268
361
  };
269
362
 
270
- const rulesMatcher = (rules, formValue = {}, apply = true, options) => {
363
+ const rulesMatcher = (rules, dataValue = {}, apply = true, options) => {
271
364
  if (!rules)
272
365
  return [apply];
273
366
  // if (typeof rules !== 'object' && !Array.isArray(rules)) {
274
367
  // throw new Error('Rules accept only array or object');
275
368
  // }
276
369
  if (typeof rules === 'function') {
277
- return [rules(formValue) === apply];
370
+ return [rules(dataValue) === apply];
278
371
  }
279
372
  const conditions = Array.isArray(rules)
280
373
  ? { $and: rules }
281
374
  : rules;
282
- const valid = rulesMatcherUtils.checkRule(conditions, formValue, options ?? {});
375
+ const valid = rulesMatcherUtils.checkRule(conditions, dataValue, options ?? {});
283
376
  return [valid === apply];
284
377
  };
285
378
 
@@ -304,9 +397,6 @@ const flattenRules = (ob) => {
304
397
  }
305
398
  return result;
306
399
  };
307
- const getRulesExists = (rules) => {
308
- return Object.keys(rules).length ? FlowUtils.mapEdge(rules) : undefined;
309
- };
310
400
  const FlowUtils = {
311
401
  generateRulesName: (nextRules) => {
312
402
  return nextRules.reduce((acc, inc) => {
@@ -363,23 +453,6 @@ const FlowUtils = {
363
453
  return res;
364
454
  },
365
455
  makeRules: (rules) => Object.entries(rules).reduce((acc2, [k, v]) => [...acc2, { nodeId: k, rules: v }], []),
366
- // TODO: This function is strictly related to React nodes, could make sense to move it in the flower-react folder
367
- generateNodesForFlowerJson: (nodes) => nodes
368
- .filter((e) => !!_get(e, 'props.id'))
369
- .map((e) => {
370
- const rules = FlowUtils.makeRules(e.props.to ?? {});
371
- const nextRules = getRulesExists(rules);
372
- const children = e.props.data?.children;
373
- return {
374
- nodeId: e.props.id,
375
- nodeType: e.type.displayName || e.props.as || 'FlowerNode',
376
- nodeTitle: _get(e.props, 'data.title'),
377
- children,
378
- nextRules,
379
- retain: e.props.retain,
380
- disabled: e.props.disabled
381
- };
382
- }),
383
456
  allEqual: (arr, arr2) => arr.length === arr2.length && arr.every((v) => arr2.includes(v)),
384
457
  findValidRule: (nextRules, value, prefix) => find(nextRules, (rule) => {
385
458
  // fix per evitare di entrare in un nodo senza regole, ma con un name,
@@ -406,7 +479,7 @@ const FlowUtils = {
406
479
  })
407
480
  };
408
481
 
409
- const FormUtils = {
482
+ const DataUtils = {
410
483
  cleanPath: (name, char = '^') => _trimStart(name, char),
411
484
  getPath: (idValue) => {
412
485
  if (!idValue) {
@@ -420,9 +493,9 @@ const FormUtils = {
420
493
  };
421
494
  }
422
495
  if (idValue.indexOf('^') === 0) {
423
- const [formName, ...rest] = FormUtils.cleanPath(idValue).split('.');
496
+ const [rootName, ...rest] = DataUtils.cleanPath(idValue).split('.');
424
497
  return {
425
- formName,
498
+ rootName,
426
499
  path: rest
427
500
  };
428
501
  }
@@ -437,7 +510,7 @@ const FormUtils = {
437
510
  */
438
511
  const CoreUtils = {
439
512
  ...FlowUtils,
440
- ...FormUtils
513
+ ...DataUtils
441
514
  };
442
515
 
443
516
  const { generateNodes, hasNode, makeObjectRules, generateRulesName, findValidRule } = CoreUtils;
@@ -650,7 +723,7 @@ const FlowerCoreBaseReducers = {
650
723
  payload: { name: flowName, node: nextNumberNode }
651
724
  });
652
725
  },
653
- prev: (state, { payload }) => {
726
+ back: (state, { payload }) => {
654
727
  const { name, flowName } = payload;
655
728
  FlowerCoreBaseReducers.historyPop(state, {
656
729
  type: 'historyPop',
@@ -674,75 +747,65 @@ const FlowerCoreBaseReducers = {
674
747
  };
675
748
 
676
749
  const { getPath } = CoreUtils;
677
- /**
678
- * formName => FlowerForm
679
- * initialData => FlowerForm
680
- * fieldName => FlowerField
681
- * fieldValue => FlowerField
682
- * errors => FlowerField
683
- * customErrors => FlowerField
684
- * fieldTouched => FlowerField
685
- * fieldDirty => FlowerField
686
- * fieldHasFocus => FlowerField
687
- */
688
750
  const FlowerCoreDataReducers = {
689
- setFormTouched: (state, { payload }) => {
690
- if (!_get(state, typeof payload === 'string' ? payload : payload.formName)) {
751
+ setFormSubmitted: (state, { payload }) => {
752
+ const rootPath = typeof payload === 'string' ? payload : payload.rootName;
753
+ if (!_get(state, [rootPath])) {
691
754
  return state;
692
755
  }
693
- _set(state, typeof payload === 'string' ? payload : payload.formName, true);
756
+ _set(state, [rootPath, 'isSubmitted'], true);
694
757
  return state;
695
758
  },
696
- formAddCustomErrors: (state, { payload }) => {
697
- _set(state, [payload.formName, 'customErrors', payload.id], payload.errors);
759
+ addCustomDataErrors: (state, { payload }) => {
760
+ _set(state, [payload.rootName, 'customErrors', payload.id], payload.errors);
698
761
  },
699
- formAddErrors: (state, { payload }) => {
700
- _set(state, [payload.formName, 'errors', payload.id], payload.errors);
762
+ addDataErrors: (state, { payload }) => {
763
+ _set(state, [payload.rootName, 'errors', payload.id], payload.errors);
701
764
  },
702
- formRemoveErrors: (state, { payload }) => {
703
- _unset(state, [payload.formName, 'errors', payload.id]);
704
- _unset(state, [payload.formName, 'customErrors', payload.id]);
705
- _unset(state, [payload.formName, 'isValidating']);
765
+ removeDataErrors: (state, { payload }) => {
766
+ _unset(state, [payload.rootName, 'errors', payload.id]);
767
+ _unset(state, [payload.rootName, 'customErrors', payload.id]);
768
+ _unset(state, [payload.rootName, 'isValidating']);
706
769
  },
707
- formFieldTouch: (state, { payload }) => {
708
- _set(state, [payload.formName, 'touches', payload.id], payload.touched);
770
+ fieldTouch: (state, { payload }) => {
771
+ _set(state, [payload.rootName, 'touches', payload.id], payload.touched);
709
772
  },
710
- formFieldDirty: (state, { payload }) => {
711
- _set(state, [payload.formName, 'dirty', payload.id], payload.dirty);
773
+ fieldDirty: (state, { payload }) => {
774
+ _set(state, [payload.rootName, 'dirty', payload.id], payload.dirty);
712
775
  },
713
- formFieldFocus: (state, { payload }) => {
776
+ fieldFocus: (state, { payload }) => {
714
777
  if (!payload.focused) {
715
- _unset(state, [payload.formName, 'hasFocus']);
778
+ _unset(state, [payload.rootName, 'hasFocus']);
716
779
  return;
717
780
  }
718
- _set(state, [payload.formName, 'hasFocus'], payload.id);
781
+ _set(state, [payload.rootName, 'hasFocus'], payload.id);
719
782
  },
720
783
  addData: (state, { payload }) => {
721
- const prevData = _get(state, [payload.formName, 'data'], {});
722
- _set(state, [payload.formName, 'data'], { ...prevData, ...payload.value });
784
+ const prevData = _get(state, [payload.rootName, 'data'], {});
785
+ _set(state, [payload.rootName, 'data'], { ...prevData, ...payload.value });
723
786
  },
724
787
  addDataByPath: (state, { payload }) => {
725
788
  const { path: newpath } = getPath(payload.id);
726
789
  if (payload.id && payload.id.length) {
727
- _set(state, [payload.formName, 'data', ...newpath], payload.value);
790
+ _set(state, [payload.rootName, 'data', ...newpath], payload.value);
728
791
  if (payload && payload.dirty) {
729
- _set(state, [payload.formName, 'dirty', payload.id], payload.dirty);
792
+ _set(state, [payload.rootName, 'dirty', payload.id], payload.dirty);
730
793
  }
731
794
  }
732
795
  },
733
796
  // TODO usato al momento solo il devtool
734
797
  replaceData: /* istanbul ignore next */ (state, { payload }) => {
735
798
  /* istanbul ignore next */
736
- _set(state, [payload.formName, 'data'], payload.value);
799
+ _set(state, [payload.rootName, 'data'], payload.value);
737
800
  },
738
801
  unsetData: (state, { payload }) => {
739
- _unset(state, [payload.formName, 'data', ...payload.id]);
802
+ _unset(state, [payload.rootName, 'data', ...payload.id]);
740
803
  },
741
- setFormIsValidating: (state, { payload }) => {
742
- _set(state, [payload.formName, 'isValidating'], payload.isValidating);
804
+ setIsDataValidating: (state, { payload }) => {
805
+ _set(state, [payload.rootName, 'isValidating'], payload.isValidating);
743
806
  },
744
- resetForm: (state, { payload: { formName, initialData } }) => {
745
- const touchedFields = _get(state, [formName, 'errors'], {});
807
+ resetData: (state, { payload: { rootName, initialData } }) => {
808
+ const touchedFields = _get(state, [rootName, 'errors'], {});
746
809
  const newStateData = initialData
747
810
  ? Object.keys(touchedFields).reduce((acc, key) => {
748
811
  const { path } = getPath(key);
@@ -751,13 +814,13 @@ const FlowerCoreDataReducers = {
751
814
  return acc;
752
815
  }, {})
753
816
  : {};
754
- _set(state, [formName, 'data'], newStateData);
755
- _unset(state, [formName, 'touches']);
756
- _unset(state, [formName, 'dirty']);
757
- _unset(state, [formName, 'isSubmitted']);
817
+ _set(state, [rootName, 'data'], newStateData);
818
+ _unset(state, [rootName, 'touches']);
819
+ _unset(state, [rootName, 'dirty']);
820
+ _unset(state, [rootName, 'isSubmitted']);
758
821
  },
759
- initForm: (state, { payload: { formName, initialData } }) => {
760
- _set(state, [formName, 'data'], initialData);
822
+ initData: (state, { payload: { rootName, initialData } }) => {
823
+ _set(state, [rootName, 'data'], initialData);
761
824
  }
762
825
  };
763
826
 
@@ -766,8 +829,6 @@ const FlowerCoreReducers = { ...FlowerCoreBaseReducers, ...FlowerCoreDataReducer
766
829
  const FlowerCoreStateBaseSelectors = {
767
830
  selectGlobal: (state) => state && state[exports.REDUCER_NAME.FLOWER_FLOW],
768
831
  selectFlower: (name) => (state) => _get(state, [name]),
769
- // selectFlowerFormNode: (id) => (state) =>
770
- // _get(state, [REDUCER_NAME.FLOWER_DATA, id]),
771
832
  selectFlowerHistory: (flower) => _get(flower, ['history'], []),
772
833
  makeSelectNodesIds: (flower) => _get(flower, ['nodes']),
773
834
  makeSelectStartNodeId: (flower) => _get(flower, ['startId']),
@@ -796,24 +857,24 @@ const FlowerCoreStateBaseSelectors = {
796
857
 
797
858
  const FlowerCoreStateDataSelectors = {
798
859
  selectGlobalReducerByName: (name) => (state) => state[name] ?? state[exports.REDUCER_NAME.FLOWER_DATA][name],
799
- selectGlobalForm: (state) => {
860
+ selectGlobalData: (state) => {
800
861
  return state && state[exports.REDUCER_NAME.FLOWER_DATA];
801
862
  },
802
863
  // getDataByFlow: (flower) => _get(flower, 'data') ?? {},
803
864
  getDataFromState: (id) => (data) => (id === '*' ? data : _get(data, id)),
804
- makeSelectNodeFormSubmitted: (form) => form && form.isSubmitted,
805
- makeSelectNodeFormFieldTouched: (id) => (form) => form && form.touches && form.touches[id],
806
- makeSelectNodeFormFieldFocused: (id) => (form) => {
807
- return form && form.hasFocus === id ? id : undefined;
808
- },
809
- makeSelectNodeFormFieldDirty: (id) => (form) => form && form.dirty && form.dirty[id],
810
- makeSelectNodeErrors: createFormData,
811
- makeSelectFieldError: (name, id, validate) => (data, form) => {
812
- const customErrors = Object.entries((form && form.customErrors) || {})
865
+ makeSelectNodeDataSubmitted: (data) => data && data.isSubmitted,
866
+ makeSelectNodeDataFieldTouched: (id) => (data) => data && data.touches && data.touches[id],
867
+ makeSelectNodeDataFieldFocused: (id) => (data) => {
868
+ return data && data.hasFocus === id ? id : undefined;
869
+ },
870
+ makeSelectNodeDataFieldDirty: (id) => (data) => data && data.dirty && data.dirty[id],
871
+ makeSelectNodeErrors: generateData,
872
+ makeSelectFieldError: (name, id, validate) => (globalData, data) => {
873
+ const customErrors = Object.entries((data && data.customErrors) || {})
813
874
  .filter(([k]) => k === id)
814
875
  .map(([, v]) => v)
815
876
  .flat();
816
- if (!validate || !data)
877
+ if (!validate || !globalData)
817
878
  return [];
818
879
  const errors = validate.filter((rule) => {
819
880
  if (!rule)
@@ -821,7 +882,7 @@ const FlowerCoreStateDataSelectors = {
821
882
  if (!rule.rules)
822
883
  return true;
823
884
  const transformSelf = CoreUtils.mapKeysDeepLodash(rule.rules, (v, key) => key === '$self' ? id : key);
824
- const [hasError] = rulesMatcher(transformSelf, data, false, {
885
+ const [hasError] = rulesMatcher(transformSelf, globalData, false, {
825
886
  prefix: name
826
887
  });
827
888
  return hasError;
@@ -829,8 +890,8 @@ const FlowerCoreStateDataSelectors = {
829
890
  const result = errors.map((r) => (r && r.message) || 'error');
830
891
  return [...customErrors, ...(result.length === 0 ? [] : result)];
831
892
  },
832
- selectorRulesDisabled: (id, rules, keys, flowName, value) => (data, form) => {
833
- const newState = { ...data, ...value, $form: form };
893
+ selectorRulesDisabled: (id, rules, keys, flowName, value) => (globalData, data) => {
894
+ const newState = { ...globalData, ...value, $data: data, $form: data };
834
895
  const state = Object.assign(newState, id ? { $self: _get(newState, [flowName, ...id.split('.')]) } : {});
835
896
  if (!rules)
836
897
  return false;
@@ -855,7 +916,9 @@ const FlowerCoreStateSelectors = {
855
916
  ...FlowerCoreStateDataSelectors
856
917
  };
857
918
 
919
+ exports.AbacEngine = AbacEngine;
858
920
  exports.CoreUtils = CoreUtils;
921
+ exports.DataUtils = DataUtils;
859
922
  exports.Emitter = Emitter;
860
923
  exports.FlowUtils = FlowUtils;
861
924
  exports.FlowerCoreBaseReducers = FlowerCoreBaseReducers;
@@ -865,9 +928,15 @@ exports.FlowerCoreStateBaseSelectors = FlowerCoreStateBaseSelectors;
865
928
  exports.FlowerCoreStateDataSelectors = FlowerCoreStateDataSelectors;
866
929
  exports.FlowerCoreStateSelectors = FlowerCoreStateSelectors;
867
930
  exports.FlowerStateUtils = FlowerStateUtils;
868
- exports.FormUtils = FormUtils;
869
- exports.createFormData = createFormData;
931
+ exports.clearSubject = clearSubject;
870
932
  exports.devtoolState = devtoolState;
871
933
  exports.flattenRules = flattenRules;
934
+ exports.generateData = generateData;
935
+ exports.getAbacEngine = getAbacEngine;
936
+ exports.getRawRules = getRawRules;
937
+ exports.getSubject = getSubject;
938
+ exports.initAbac = initAbac;
939
+ exports.isAbacInitialized = isAbacInitialized;
872
940
  exports.rulesMatcher = rulesMatcher;
873
941
  exports.rulesMatcherUtils = rulesMatcherUtils;
942
+ exports.setSubject = setSubject;