@icure/form 3.1.0 → 3.1.1

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.
@@ -12,8 +12,8 @@ import { sortedBy } from '../utils/no-lodash';
12
12
  import { areCodesEqual, codeStubToCode, contentToPrimitiveType, isContentEqual, primitiveTypeToContent } from './icure-utils';
13
13
  import { parsePrimitive } from '../utils/primitive';
14
14
  import { anyDateToDate, dateToFuzzyDate } from '../utils/dates';
15
- import { v4 as uuidv4 } from 'uuid';
16
15
  import { normalizeCodes } from '../utils/code-utils';
16
+ let _ordinal = 0;
17
17
  function notify(l, fvc, changedKeys, force = false) {
18
18
  l(fvc, changedKeys, force);
19
19
  }
@@ -84,11 +84,12 @@ export class BridgedFormValuesContainer {
84
84
  this.changeListeners = changeListeners;
85
85
  this.interpreterContext = interpreterContext;
86
86
  this.dependenciesCache = dependenciesCache;
87
- this._id = uuidv4();
87
+ this._id = '';
88
88
  this.dependentValuesComputationIteration = undefined;
89
- console.log(`Creating bridge FVC (${contactFormValuesContainer.rootForm.formTemplateId}) with ${contactFormValuesContainer.children.length} children [${this._id}]`);
90
89
  //Before start to broadcast changes, we need to fill in the contactFormValuesContainer with the dependent values
91
90
  this.contactFormValuesContainer = contactFormValuesContainer;
91
+ this._id = `[${contactFormValuesContainer._id}]`;
92
+ console.log(`+ BFVC ${this._id} created`);
92
93
  this.mutateAndNotify = (newContactFormValuesContainer, changedKeys, force = false) => {
93
94
  if (this.changeListeners.length === 0) {
94
95
  return;
@@ -277,7 +278,7 @@ export class BridgedFormValuesContainer {
277
278
  return __awaiter(this, void 0, void 0, function* () {
278
279
  const iterate = (changedKeys, iterationCount) => __awaiter(this, void 0, void 0, function* () {
279
280
  if (this.contactFormValuesContainer.rootForm.formTemplateId) {
280
- console.log(`Starting iteration ${iterationCount} with changed keys : ${changedKeys ? changedKeys.join(', ') : 'all'}`);
281
+ console.log(`% ${iterationCount}, keys: ${changedKeys ? changedKeys.join(', ') : 'all'}`);
281
282
  return yield Promise.all(this.dependentValuesProvider(this.contactFormValuesContainer.rootForm.descr, this.contactFormValuesContainer.rootForm.formTemplateId).map((_a) => __awaiter(this, [_a], void 0, function* ({ metadata, revisionsFilter, formula }) {
282
283
  var _b, _c;
283
284
  try {
@@ -339,7 +340,12 @@ export class BridgedFormValuesContainer {
339
340
  allChangedKeys.push(...(changedKeys !== null && changedKeys !== void 0 ? changedKeys : []));
340
341
  };
341
342
  changes.forEach(([id, metadata, language, newValue]) => {
342
- setValueOnContactFormValuesContainer(this.contactFormValuesContainer, metadata.label, language, newValue !== null && newValue !== void 0 ? newValue : undefined, id, metadata, interceptor);
343
+ try {
344
+ setValueOnContactFormValuesContainer(this.contactFormValuesContainer, metadata.label, language, newValue !== null && newValue !== void 0 ? newValue : undefined, id, metadata, interceptor);
345
+ }
346
+ catch (e) {
347
+ console.log(`Error while applying dependent value change for ${metadata.label} (${id})`, e);
348
+ }
343
349
  });
344
350
  return allChangedKeys;
345
351
  }
@@ -385,6 +391,7 @@ export class BridgedFormValuesContainer {
385
391
  .join(', ');
386
392
  };
387
393
  const log = console.log;
394
+ // noinspection JSUnusedGlobalSymbols
388
395
  const native = {
389
396
  parseInt: parseInt,
390
397
  parseFloat: parseFloat,
@@ -446,9 +453,7 @@ export class BridgedFormValuesContainer {
446
453
  }
447
454
  getChildren() {
448
455
  return __awaiter(this, void 0, void 0, function* () {
449
- const children = yield Promise.all((yield this.contactFormValuesContainer.getChildren()).map((fvc) => new BridgedFormValuesContainer(this.responsible, fvc, this.interpreter, this.contact, this.initialValuesProvider, this.dependentValuesProvider, this.validatorsProvider, this.language, [], this.interpreterContext).init()));
450
- console.log(`${children.length} children found in ${this.contactFormValuesContainer.rootForm.formTemplateId} initialised with `, this.initialValuesProvider);
451
- return children;
456
+ return yield Promise.all((yield this.contactFormValuesContainer.getChildren()).map((fvc) => new BridgedFormValuesContainer(this.responsible, fvc, this.interpreter, this.contact, this.initialValuesProvider, this.dependentValuesProvider, this.validatorsProvider, this.language, [], this.interpreterContext).init()));
452
457
  });
453
458
  }
454
459
  getValidationErrors() {
@@ -518,17 +523,20 @@ export class ContactFormValuesContainer {
518
523
  allForms() {
519
524
  return [this.rootForm].concat(this.children.flatMap((c) => c.allForms()));
520
525
  }
521
- constructor(rootForm, currentContact, contactsHistory, serviceFactory, children, formFactory, formRecycler, changeListeners = [], initialised = true) {
522
- this._id = uuidv4();
526
+ constructor(rootForm, currentContact, contactsHistory, serviceFactory, children, formFactory, formRecycler, changeListeners = [], anchorId, initialised = true) {
527
+ var _a;
528
+ this._id = '';
523
529
  this._initialised = false;
524
- console.log(`Creating contact FVC (${rootForm.formTemplateId}) with ${children.length} children [${this._id}]`);
530
+ this.rootForm = rootForm;
531
+ this._id = `${((_a = rootForm.formTemplateId) !== null && _a !== void 0 ? _a : '').substring(0, 4)}.${++_ordinal}`;
532
+ console.log(`+ CFVC: ${this._id} with children { ${children.map((c) => c._id).join(', ')} }`);
525
533
  if (contactsHistory.includes(currentContact)) {
526
534
  throw new Error('Illegal argument, the history must not contain the currentContact');
527
535
  }
528
- this.rootForm = rootForm;
529
536
  this.currentContact = currentContact;
530
537
  this.contactsHistory = sortedBy(contactsHistory, 'created', 'desc');
531
538
  this.children = children;
539
+ this.anchorId = anchorId;
532
540
  this.serviceFactory = serviceFactory;
533
541
  this.formFactory = formFactory;
534
542
  this.formRecycler = formRecycler;
@@ -536,7 +544,7 @@ export class ContactFormValuesContainer {
536
544
  this._initialised = initialised;
537
545
  this.indexedServices = [this.currentContact].concat(this.contactsHistory).reduce((acc, ctc) => {
538
546
  var _a, _b, _c;
539
- const services = (_c = (_b = (_a = ctc.services) === null || _a === void 0 ? void 0 : _a.filter((s) => { var _a; return (_a = ctc.subContacts) === null || _a === void 0 ? void 0 : _a.some((sc) => { var _a; return sc.formId === this.rootForm.id && ((_a = sc.services) === null || _a === void 0 ? void 0 : _a.some((sss) => sss.serviceId === s.id)); }); })) === null || _b === void 0 ? void 0 : _b.reduce((acc, s) => {
547
+ return ((_c = (_b = (_a = ctc.services) === null || _a === void 0 ? void 0 : _a.filter((s) => { var _a; return (_a = ctc.subContacts) === null || _a === void 0 ? void 0 : _a.some((sc) => { var _a; return sc.formId === this.rootForm.id && ((_a = sc.services) === null || _a === void 0 ? void 0 : _a.some((sss) => sss.serviceId === s.id)); }); })) === null || _b === void 0 ? void 0 : _b.reduce((acc, s) => {
540
548
  var _a, _b;
541
549
  return s.id
542
550
  ? Object.assign(Object.assign({}, acc), { [s.id]: ((_a = acc[s.id]) !== null && _a !== void 0 ? _a : (acc[s.id] = [])).concat({
@@ -544,26 +552,29 @@ export class ContactFormValuesContainer {
544
552
  modified: ctc.created,
545
553
  value: s,
546
554
  }) }) : acc;
547
- }, acc)) !== null && _c !== void 0 ? _c : acc;
548
- return services;
555
+ }, acc)) !== null && _c !== void 0 ? _c : acc);
549
556
  }, {});
550
557
  this.synchronise();
551
558
  }
552
559
  synchronise() {
553
560
  this.children.forEach((childFVC) => {
554
- this.registerChildFormValuesContainer(childFVC.synchronise());
561
+ var _a;
562
+ this.registerChildFormValuesContainer(childFVC.synchronise(), (_a = childFVC.anchorId) !== null && _a !== void 0 ? _a : '');
555
563
  });
556
564
  return this;
557
565
  }
558
566
  //Make sure that when a child is changed, a new version of this is created with the updated child
559
- registerChildFormValuesContainer(childFormValueContainer) {
567
+ registerChildFormValuesContainer(childFormValueContainer, anchorId) {
560
568
  childFormValueContainer.changeListeners = [
561
- (newValue) => {
569
+ (newValue, changedKeys, force) => {
570
+ if ((changedKeys === null || changedKeys === void 0 ? void 0 : changedKeys.length) === 0 && !force) {
571
+ return;
572
+ }
562
573
  console.log(`Child ${newValue._id} ${childFormValueContainer.rootForm.formTemplateId} changed, updating parent ${this._id} ${this.rootForm.formTemplateId}`);
563
574
  const newContactFormValuesContainer = new ContactFormValuesContainer(this.rootForm, this.currentContact, this.contactsHistory, this.serviceFactory, this.children.map((c) => {
564
575
  return c.rootForm.id === childFormValueContainer.rootForm.id ? newValue : c;
565
- }), this.formFactory, this.formRecycler);
566
- this.changeListeners.forEach((l) => notify(l, newContactFormValuesContainer, [] /* no changed keys except the anchor*/));
576
+ }), this.formFactory, this.formRecycler, [], this.anchorId, true);
577
+ this.changeListeners.forEach((l) => notify(l, newContactFormValuesContainer, null));
567
578
  },
568
579
  ];
569
580
  }
@@ -573,8 +584,8 @@ export class ContactFormValuesContainer {
573
584
  ? yield Promise.all((yield formChildrenProvider(rootForm.id)).map((f) => __awaiter(this, void 0, void 0, function* () {
574
585
  // eslint-disable-next-line max-len
575
586
  return yield ContactFormValuesContainer.fromFormsHierarchy(f, currentContact, contactsHistory, serviceFactory, formChildrenProvider, formFactory, formRecycler); })))
576
- : [], formFactory, formRecycler, changeListeners, false);
577
- contactFormValuesContainer.children.forEach((childFVC) => contactFormValuesContainer.registerChildFormValuesContainer(childFVC));
587
+ : [], formFactory, formRecycler, changeListeners, rootForm.descr, false);
588
+ contactFormValuesContainer.children.forEach((childFVC) => { var _a; return contactFormValuesContainer.registerChildFormValuesContainer(childFVC, (_a = childFVC.anchorId) !== null && _a !== void 0 ? _a : 'all'); });
578
589
  return contactFormValuesContainer;
579
590
  });
580
591
  }
@@ -646,8 +657,8 @@ export class ContactFormValuesContainer {
646
657
  meta.valueDate && (newService.valueDate = meta.valueDate);
647
658
  meta.codes && (newService.codes = normalizeCodes(meta.codes));
648
659
  meta.tags && (newService.tags = normalizeCodes(meta.tags));
649
- const newFormValuesContainer = new ContactFormValuesContainer(this.rootForm, new DecryptedContact(Object.assign(Object.assign({}, this.currentContact), { services: (_a = this.currentContact.services) === null || _a === void 0 ? void 0 : _a.map((s) => (s.id === service.id ? newService : s)) })), this.contactsHistory, this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners);
650
- this.changeListeners.forEach((l) => notify(l, newFormValuesContainer, [meta.label] /* only the label is guaranteed to be changed, but it's the only info we have at this point */));
660
+ const newFormValuesContainer = new ContactFormValuesContainer(this.rootForm, new DecryptedContact(Object.assign(Object.assign({}, this.currentContact), { services: (_a = this.currentContact.services) === null || _a === void 0 ? void 0 : _a.map((s) => (s.id === service.id ? newService : s)) })), this.contactsHistory, this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners, this.anchorId, true);
661
+ this.changeListeners.forEach((l) => notify(l, newFormValuesContainer, [meta.label]));
651
662
  }
652
663
  }
653
664
  setValue(label, language, value, id, metadata, changeListenersOverrider) {
@@ -711,7 +722,7 @@ export class ContactFormValuesContainer {
711
722
  ? ((_v = this.currentContact.services) !== null && _v !== void 0 ? _v : []).map((s) => (s.id === service.id ? newService : s))
712
723
  : [...((_w = this.currentContact.services) !== null && _w !== void 0 ? _w : []), newService] }));
713
724
  }
714
- const newFormValuesContainer = new ContactFormValuesContainer(this.rootForm, newCurrentContact, this.contactsHistory.map((c) => (c === this.currentContact ? newCurrentContact : c)), this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners);
725
+ const newFormValuesContainer = new ContactFormValuesContainer(this.rootForm, newCurrentContact, this.contactsHistory.map((c) => (c === this.currentContact ? newCurrentContact : c)), this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners, this.anchorId, true);
715
726
  changeListenersOverrider ? changeListenersOverrider(newFormValuesContainer, [label]) : this.changeListeners.forEach((l) => notify(l, newFormValuesContainer, [label]));
716
727
  }
717
728
  }
@@ -721,8 +732,8 @@ export class ContactFormValuesContainer {
721
732
  if (service) {
722
733
  const newFormValuesContainer = new ContactFormValuesContainer(this.rootForm, new DecryptedContact(Object.assign(Object.assign({}, this.currentContact), { services: (_a = this.currentContact.services) === null || _a === void 0 ? void 0 : _a.map((s) => s.id === serviceId
723
734
  ? new DecryptedService(Object.assign(Object.assign({}, service), { endOfLife: Date.now() }))
724
- : s) })), this.contactsHistory, this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners);
725
- this.changeListeners.forEach((l) => notify(l, newFormValuesContainer, []));
735
+ : s) })), this.contactsHistory, this.serviceFactory, this.children, this.formFactory, this.formRecycler, this.changeListeners, this.anchorId, true);
736
+ this.changeListeners.forEach((l) => { var _a; return notify(l, newFormValuesContainer, [(_a = service.label) !== null && _a !== void 0 ? _a : service.id] /* only the label is going to propagate the changes for the formulas */); });
726
737
  }
727
738
  }
728
739
  toMarkdownTable() {
@@ -788,10 +799,12 @@ export class ContactFormValuesContainer {
788
799
  if (!parentId)
789
800
  return;
790
801
  const newForm = yield this.formFactory(parentId, anchorId, templateId, label);
791
- const childFVC = new ContactFormValuesContainer(newForm, this.currentContact, this.contactsHistory, this.serviceFactory, [], this.formFactory, this.formRecycler, [], false);
792
- const newContactFormValuesContainer = new ContactFormValuesContainer(this.rootForm, this.currentContact, this.contactsHistory, this.serviceFactory, [...this.children, childFVC], this.formFactory, this.formRecycler, this.changeListeners);
793
- newContactFormValuesContainer.registerChildFormValuesContainer(childFVC);
794
- this.changeListeners.forEach((l) => notify(l, newContactFormValuesContainer, []));
802
+ const childFVC = new ContactFormValuesContainer(newForm, this.currentContact, this.contactsHistory, this.serviceFactory, [], this.formFactory, this.formRecycler, [], anchorId, false);
803
+ const newContactFormValuesContainer = new ContactFormValuesContainer(this.rootForm, this.currentContact, this.contactsHistory, this.serviceFactory, [...this.children, childFVC], this.formFactory, this.formRecycler, this.changeListeners, this.anchorId, true);
804
+ newContactFormValuesContainer.registerChildFormValuesContainer(childFVC, anchorId);
805
+ this.changeListeners.forEach((l) => {
806
+ notify(l, newContactFormValuesContainer, null);
807
+ });
795
808
  });
796
809
  }
797
810
  getServiceInCurrentContact(id) {
@@ -801,13 +814,84 @@ export class ContactFormValuesContainer {
801
814
  }
802
815
  removeChild(container) {
803
816
  return __awaiter(this, void 0, void 0, function* () {
804
- const newContactFormValuesContainer = new ContactFormValuesContainer(this.rootForm, this.currentContact, this.contactsHistory, this.serviceFactory, this.children.filter((c) => c.rootForm.id !== container.rootForm.id), this.formFactory, this.formRecycler, this.changeListeners);
805
- this.changeListeners.forEach((l) => notify(l, newContactFormValuesContainer, []));
817
+ const newContactFormValuesContainer = new ContactFormValuesContainer(this.rootForm, this.currentContact, this.contactsHistory, this.serviceFactory, this.children.filter((c) => c.rootForm.id !== container.rootForm.id), this.formFactory, this.formRecycler, this.changeListeners, this.anchorId, true);
818
+ this.changeListeners.forEach((l) => { var _a; return notify(l, newContactFormValuesContainer, [`subForms_${(_a = container.anchorId) !== null && _a !== void 0 ? _a : 'all'}`]); });
806
819
  });
807
820
  }
808
821
  }
822
+ // Runtime validation for FieldValue — guards against mistyped data coming from
823
+ // external callers (formulas, SDK bridges, etc.) where the TypeScript types may
824
+ // not match the actual runtime values.
825
+ const validatePrimitiveType = (pt, path) => {
826
+ const v = pt.value;
827
+ switch (pt.type) {
828
+ case 'string':
829
+ if (typeof v !== 'string') {
830
+ throw new Error(`${path}: expected string value, got ${typeof v}`);
831
+ }
832
+ break;
833
+ case 'number':
834
+ if (typeof v !== 'number') {
835
+ throw new Error(`${path}: expected number value, got ${typeof v}`);
836
+ }
837
+ break;
838
+ case 'boolean':
839
+ if (typeof v !== 'boolean') {
840
+ throw new Error(`${path}: expected boolean value, got ${typeof v}`);
841
+ }
842
+ break;
843
+ case 'timestamp':
844
+ if (typeof v !== 'number') {
845
+ throw new Error(`${path}: expected number value for timestamp, got ${typeof v}`);
846
+ }
847
+ break;
848
+ case 'datetime':
849
+ if (typeof v !== 'number') {
850
+ throw new Error(`${path}: expected number value for datetime, got ${typeof v}`);
851
+ }
852
+ break;
853
+ case 'measure': {
854
+ const m = pt;
855
+ if (m.value !== undefined && typeof m.value !== 'number') {
856
+ throw new Error(`${path}: expected number or undefined value for measure, got ${typeof m.value}`);
857
+ }
858
+ if (m.unit !== undefined && typeof m.unit !== 'string') {
859
+ throw new Error(`${path}: expected string or undefined unit for measure, got ${typeof m.unit}`);
860
+ }
861
+ break;
862
+ }
863
+ case 'compound':
864
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
865
+ throw new Error(`${path}: expected object value for compound, got ${Array.isArray(v) ? 'array' : typeof v}`);
866
+ }
867
+ for (const [key, subValue] of Object.entries(v)) {
868
+ validatePrimitiveType(subValue, `${path}.compound[${key}]`);
869
+ }
870
+ break;
871
+ default:
872
+ throw new Error(`${path}: unknown PrimitiveType type: ${pt.type}`);
873
+ }
874
+ };
875
+ const validateCode = (code, path) => {
876
+ const id = code.id;
877
+ if (!id || typeof id !== 'string') {
878
+ throw new Error(`${path}: code must have a non-empty string id, got ${JSON.stringify(id)}`);
879
+ }
880
+ };
881
+ const validateFieldValue = (fv, language, label) => {
882
+ const pt = fv.content[language];
883
+ if (pt) {
884
+ validatePrimitiveType(pt, `FieldValue[${label}].content[${language}]`);
885
+ }
886
+ if (fv.codes) {
887
+ fv.codes.forEach((code, i) => validateCode(code, `FieldValue[${label}].codes[${i}]`));
888
+ }
889
+ };
809
890
  const setValueOnContactFormValuesContainer = (cfvc, label, language, fv, id, metadata, changeListenersOverrider) => {
810
891
  var _a, _b, _c, _d;
892
+ if (fv) {
893
+ validateFieldValue(fv, language, label);
894
+ }
811
895
  const value = fv === null || fv === void 0 ? void 0 : fv.content[language];
812
896
  cfvc.setValue(label, language, new DecryptedService({
813
897
  id: id,