@revisium/schema-toolkit 0.11.1 → 0.12.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.
@@ -1,5 +1,7 @@
1
1
  import { fileSchema, rowSchemaHashSchema, rowHashSchema, rowUpdatedAtSchema, rowPublishedAtSchema, rowCreatedAtSchema, rowCreatedIdSchema, rowVersionIdSchema, rowIdSchema } from './chunk-LLHQWDAR.js';
2
2
  import { JsonArrayStore, JsonObjectStore, JsonStringStore, JsonNumberStore, JsonBooleanStore, JsonObjectValueStore, JsonArrayValueStore, createJsonValueStore, getTransformation } from './chunk-HY4VE7H2.js';
3
+ import { parseExpression, buildDependencyGraph, detectCircularDependencies, validateFormulaSyntax, inferFormulaType, parseFormula, getTopologicalOrder, evaluateWithContext } from '@revisium/formula';
4
+ export { formulaSpec } from '@revisium/formula/spec';
3
5
 
4
6
  // src/lib/createJsonSchemaStore.ts
5
7
  var createJsonSchemaStore = (schema, refs = {}) => {
@@ -569,6 +571,497 @@ function createChange(path, oldValue, newValue, changeType) {
569
571
  return { path, oldValue, newValue, changeType };
570
572
  }
571
573
 
574
+ // src/lib/extract-schema-formulas.ts
575
+ function extractSchemaFormulas(schema) {
576
+ const formulas = [];
577
+ extractFormulasRecursive(schema, "", formulas);
578
+ return formulas;
579
+ }
580
+ function extractFormulasRecursive(schema, pathPrefix, formulas) {
581
+ if (schema.type === "array" && schema.items) {
582
+ extractFormulasRecursive(schema.items, `${pathPrefix}[]`, formulas);
583
+ return;
584
+ }
585
+ const properties = schema.properties ?? {};
586
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
587
+ const fullPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
588
+ const xFormula = fieldSchema["x-formula"];
589
+ if (xFormula) {
590
+ formulas.push({
591
+ fieldName: fullPath,
592
+ expression: xFormula.expression,
593
+ fieldType: fieldSchema.type ?? "string"
594
+ });
595
+ }
596
+ if (fieldSchema.type === "object" && fieldSchema.properties) {
597
+ extractFormulasRecursive(fieldSchema, fullPath, formulas);
598
+ }
599
+ if (fieldSchema.type === "array" && fieldSchema.items) {
600
+ extractFormulasRecursive(fieldSchema.items, `${fullPath}[]`, formulas);
601
+ }
602
+ }
603
+ }
604
+ function validateSchemaFormulas(schema) {
605
+ const errors = [];
606
+ const formulas = extractSchemaFormulas(schema);
607
+ for (const formula of formulas) {
608
+ const error = validateFormulaAgainstSchema(
609
+ formula.expression,
610
+ formula.fieldName,
611
+ schema
612
+ );
613
+ if (error) {
614
+ errors.push(error);
615
+ }
616
+ }
617
+ if (errors.length > 0) {
618
+ return { isValid: false, errors };
619
+ }
620
+ const dependencies = {};
621
+ for (const formula of formulas) {
622
+ const parseResult = parseExpression(formula.expression);
623
+ const parentPath = getParentPath(formula.fieldName);
624
+ const prefix = parentPath ? `${parentPath}.` : "";
625
+ dependencies[formula.fieldName] = parseResult.dependencies.map((dep) => {
626
+ if (dep.startsWith("/")) {
627
+ return extractFieldRoot(dep.slice(1));
628
+ }
629
+ if (dep.startsWith("../")) {
630
+ const fieldWithoutPrefix = dep.replace(/^(\.\.\/)+/, "");
631
+ return extractFieldRoot(fieldWithoutPrefix);
632
+ }
633
+ const rootField = extractFieldRoot(dep);
634
+ return `${prefix}${rootField}`;
635
+ });
636
+ }
637
+ const graph = buildDependencyGraph(dependencies);
638
+ const circularCheck = detectCircularDependencies(graph);
639
+ const cycle = circularCheck.cycle;
640
+ if (circularCheck.hasCircular && cycle && cycle.length > 0) {
641
+ const firstField = cycle[0];
642
+ if (firstField) {
643
+ errors.push({
644
+ field: firstField,
645
+ error: `Circular dependency: ${cycle.join(" \u2192 ")}`
646
+ });
647
+ return { isValid: false, errors };
648
+ }
649
+ }
650
+ return { isValid: true, errors: [] };
651
+ }
652
+ function validateFormulaAgainstSchema(expression, fieldName, schema) {
653
+ return validateFormulaInContext(expression, fieldName, schema);
654
+ }
655
+ function validateFormulaInContext(expression, fieldPath, rootSchema) {
656
+ const syntaxResult = validateFormulaSyntax(expression);
657
+ if (!syntaxResult.isValid) {
658
+ return {
659
+ field: fieldPath,
660
+ error: syntaxResult.error,
661
+ position: syntaxResult.position
662
+ };
663
+ }
664
+ const parentPath = getParentPath(fieldPath);
665
+ const localFieldName = getFieldName(fieldPath);
666
+ const contextSchema = resolveSubSchema(rootSchema, parentPath);
667
+ if (!contextSchema) {
668
+ return {
669
+ field: fieldPath,
670
+ error: `Cannot resolve schema context for path '${parentPath}'`
671
+ };
672
+ }
673
+ const parseResult = parseExpression(expression);
674
+ const localSchemaFields = getSchemaFields(contextSchema);
675
+ const rootSchemaFields = getSchemaFields(rootSchema);
676
+ for (const dep of parseResult.dependencies) {
677
+ if (dep.startsWith("/")) {
678
+ const rootField = extractFieldRoot(dep.slice(1));
679
+ if (!rootSchemaFields.has(rootField)) {
680
+ return {
681
+ field: fieldPath,
682
+ error: `Unknown root field '${rootField}' in formula`
683
+ };
684
+ }
685
+ } else if (dep.startsWith("../")) {
686
+ const fieldWithoutPrefix = dep.replace(/^(\.\.\/)+/, "");
687
+ const rootField = extractFieldRoot(fieldWithoutPrefix);
688
+ if (!rootSchemaFields.has(rootField)) {
689
+ return {
690
+ field: fieldPath,
691
+ error: `Unknown root field '${rootField}' in formula`
692
+ };
693
+ }
694
+ } else {
695
+ const rootField = extractFieldRoot(dep);
696
+ if (!localSchemaFields.has(rootField)) {
697
+ return {
698
+ field: fieldPath,
699
+ error: `Unknown field '${rootField}' in formula`
700
+ };
701
+ }
702
+ }
703
+ }
704
+ if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
705
+ return {
706
+ field: fieldPath,
707
+ error: `Formula cannot reference itself`
708
+ };
709
+ }
710
+ const fieldSchema = contextSchema.properties?.[localFieldName];
711
+ const expectedType = schemaTypeToInferred(fieldSchema?.type);
712
+ const fieldTypes = getSchemaFieldTypes(contextSchema);
713
+ const inferredType = inferFormulaType(expression, fieldTypes);
714
+ if (!isTypeCompatible(inferredType, expectedType)) {
715
+ return {
716
+ field: fieldPath,
717
+ error: `Type mismatch: formula returns '${inferredType}' but field expects '${expectedType}'`
718
+ };
719
+ }
720
+ return null;
721
+ }
722
+ function resolveSubSchema(schema, fieldPath) {
723
+ if (!fieldPath) {
724
+ return schema;
725
+ }
726
+ const segments = parsePathSegments2(fieldPath);
727
+ let current = schema;
728
+ for (const segment of segments) {
729
+ if (segment === "[]") {
730
+ if (current.type === "array" && current.items) {
731
+ current = current.items;
732
+ } else {
733
+ return null;
734
+ }
735
+ } else if (current.properties?.[segment]) {
736
+ current = current.properties[segment];
737
+ } else {
738
+ return null;
739
+ }
740
+ }
741
+ return current;
742
+ }
743
+ function parsePathSegments2(path) {
744
+ const segments = [];
745
+ let current = "";
746
+ let inBracket = false;
747
+ for (const char of path) {
748
+ if (char === "[") {
749
+ if (current) {
750
+ segments.push(current);
751
+ current = "";
752
+ }
753
+ inBracket = true;
754
+ } else if (char === "]") {
755
+ inBracket = false;
756
+ segments.push("[]");
757
+ } else if (char === "." && !inBracket) {
758
+ if (current) {
759
+ segments.push(current);
760
+ current = "";
761
+ }
762
+ } else if (!inBracket) {
763
+ current += char;
764
+ }
765
+ }
766
+ if (current) {
767
+ segments.push(current);
768
+ }
769
+ return segments;
770
+ }
771
+ function getParentPath(fieldPath) {
772
+ const lastDotIndex = fieldPath.lastIndexOf(".");
773
+ const lastBracketIndex = fieldPath.lastIndexOf("[");
774
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
775
+ if (splitIndex <= 0) {
776
+ return "";
777
+ }
778
+ return fieldPath.substring(0, splitIndex);
779
+ }
780
+ function getFieldName(fieldPath) {
781
+ const lastDotIndex = fieldPath.lastIndexOf(".");
782
+ const lastBracketIndex = fieldPath.lastIndexOf("]");
783
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
784
+ if (splitIndex === -1) {
785
+ return fieldPath;
786
+ }
787
+ return fieldPath.substring(splitIndex + 1);
788
+ }
789
+ function getSchemaFields(schema) {
790
+ const fields = /* @__PURE__ */ new Set();
791
+ const properties = schema.properties ?? {};
792
+ for (const fieldName of Object.keys(properties)) {
793
+ fields.add(fieldName);
794
+ }
795
+ return fields;
796
+ }
797
+ function getSchemaFieldTypes(schema) {
798
+ const fieldTypes = {};
799
+ const properties = schema.properties ?? {};
800
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
801
+ const schemaType = fieldSchema.type;
802
+ if (schemaType === "integer") {
803
+ fieldTypes[fieldName] = "number";
804
+ } else if (schemaType === "number" || schemaType === "string" || schemaType === "boolean" || schemaType === "object" || schemaType === "array") {
805
+ fieldTypes[fieldName] = schemaType;
806
+ }
807
+ }
808
+ return fieldTypes;
809
+ }
810
+ function schemaTypeToInferred(schemaType) {
811
+ if (schemaType === "number" || schemaType === "integer") return "number";
812
+ if (schemaType === "string") return "string";
813
+ if (schemaType === "boolean") return "boolean";
814
+ return null;
815
+ }
816
+ function isTypeCompatible(inferredType, expectedType) {
817
+ if (expectedType === null) return true;
818
+ if (inferredType === "unknown") return true;
819
+ return inferredType === expectedType;
820
+ }
821
+ function extractFieldRoot(dependency) {
822
+ const root = dependency.split(".")[0]?.split("[")[0];
823
+ return root || dependency;
824
+ }
825
+
826
+ // src/lib/formula.ts
827
+ function prepareFormulas(schema) {
828
+ const formulas = extractSchemaFormulas(schema);
829
+ if (formulas.length === 0) {
830
+ return [];
831
+ }
832
+ const formulasWithMeta = formulas.map((f) => enrichFormula(f));
833
+ if (formulas.length <= 1) {
834
+ return formulasWithMeta;
835
+ }
836
+ return orderByDependencies(formulasWithMeta);
837
+ }
838
+ function evaluateFormulas(formulas, data, options = {}) {
839
+ const values = {};
840
+ const errors = [];
841
+ const failedFields = /* @__PURE__ */ new Set();
842
+ for (const formula of formulas) {
843
+ const hasDependencyFailure = formula.dependencies.some(
844
+ (dep) => failedFields.has(dep)
845
+ );
846
+ if (hasDependencyFailure) {
847
+ failedFields.add(formula.fieldName);
848
+ if (options.useDefaults) {
849
+ const defaultValue = getDefaultValue(formula, options.defaults);
850
+ setValueByPath2(values, formula.fieldName, defaultValue);
851
+ setValueByPath2(data, formula.fieldName, defaultValue);
852
+ }
853
+ errors.push(
854
+ createError(formula, "Dependency formula failed", options.useDefaults ?? false)
855
+ );
856
+ continue;
857
+ }
858
+ const formulaErrors = evaluateFormula(formula, data, values, options);
859
+ if (formulaErrors.length > 0) {
860
+ errors.push(...formulaErrors);
861
+ failedFields.add(formula.fieldName);
862
+ }
863
+ }
864
+ return { values, errors };
865
+ }
866
+ function enrichFormula(formula) {
867
+ const dependencies = parseDependencies(formula.expression);
868
+ const pathInfo = parseArrayItemPath(formula.fieldName);
869
+ return { ...formula, dependencies, ...pathInfo };
870
+ }
871
+ function parseDependencies(expression) {
872
+ try {
873
+ return parseFormula(expression).dependencies;
874
+ } catch {
875
+ return [];
876
+ }
877
+ }
878
+ function parseArrayItemPath(fieldName) {
879
+ const bracketIndex = fieldName.indexOf("[]");
880
+ if (bracketIndex === -1) {
881
+ return { isArrayItem: false, arrayPath: null, localFieldPath: fieldName };
882
+ }
883
+ const arrayPath = fieldName.slice(0, bracketIndex);
884
+ const afterBrackets = fieldName.slice(bracketIndex + 2);
885
+ const localFieldPath = afterBrackets.startsWith(".") ? afterBrackets.slice(1) : afterBrackets;
886
+ return { isArrayItem: true, arrayPath, localFieldPath };
887
+ }
888
+ function orderByDependencies(formulas) {
889
+ const dependencies = Object.fromEntries(
890
+ formulas.map((f) => [f.fieldName, f.dependencies])
891
+ );
892
+ const result = getTopologicalOrder(buildDependencyGraph(dependencies));
893
+ if (!result.success) {
894
+ throw new Error(
895
+ `Cyclic dependency detected in formulas: ${result.error ?? "unknown error"}`
896
+ );
897
+ }
898
+ const formulaMap = new Map(formulas.map((f) => [f.fieldName, f]));
899
+ return result.order.map((name) => formulaMap.get(name)).filter((f) => f !== void 0);
900
+ }
901
+ function evaluateFormula(formula, data, values, options) {
902
+ if (formula.isArrayItem && formula.arrayPath) {
903
+ return evaluateArrayFormula(formula, data, values, options);
904
+ }
905
+ return evaluateSingleFormula(formula, data, values, options);
906
+ }
907
+ function evaluateSingleFormula(formula, data, values, options) {
908
+ const parentPath = getParentPath2(formula.fieldName);
909
+ const itemData = parentPath ? getValueByPath2(data, parentPath) : void 0;
910
+ const context = {
911
+ rootData: data,
912
+ ...itemData && { itemData, currentPath: parentPath }
913
+ };
914
+ try {
915
+ const result = evaluateWithContext(formula.expression, context);
916
+ if (result === void 0) {
917
+ if (options.useDefaults) {
918
+ const defaultValue = getDefaultValue(formula, options.defaults);
919
+ setValueByPath2(values, formula.fieldName, defaultValue);
920
+ setValueByPath2(data, formula.fieldName, defaultValue);
921
+ }
922
+ return [
923
+ createError(
924
+ formula,
925
+ "Formula returned undefined",
926
+ options.useDefaults ?? false
927
+ )
928
+ ];
929
+ }
930
+ setValueByPath2(values, formula.fieldName, result);
931
+ setValueByPath2(data, formula.fieldName, result);
932
+ return [];
933
+ } catch (error) {
934
+ if (options.useDefaults) {
935
+ const defaultValue = getDefaultValue(formula, options.defaults);
936
+ setValueByPath2(values, formula.fieldName, defaultValue);
937
+ setValueByPath2(data, formula.fieldName, defaultValue);
938
+ }
939
+ return [
940
+ createError(
941
+ formula,
942
+ error instanceof Error ? error.message : String(error),
943
+ options.useDefaults ?? false
944
+ )
945
+ ];
946
+ }
947
+ }
948
+ function evaluateArrayFormula(formula, data, values, options) {
949
+ const errors = [];
950
+ const arrayPath = formula.arrayPath;
951
+ const array = getValueByPath2(data, arrayPath);
952
+ if (!Array.isArray(array)) {
953
+ return errors;
954
+ }
955
+ for (let i = 0; i < array.length; i++) {
956
+ const item = array[i];
957
+ if (typeof item !== "object" || item === null) {
958
+ continue;
959
+ }
960
+ const itemData = item;
961
+ const fieldPath = `${arrayPath}[${i}].${formula.localFieldPath}`;
962
+ const context = {
963
+ rootData: data,
964
+ itemData,
965
+ currentPath: `${arrayPath}[${i}]`
966
+ };
967
+ try {
968
+ const result = evaluateWithContext(formula.expression, context);
969
+ if (result === void 0) {
970
+ if (options.useDefaults) {
971
+ const defaultValue = getDefaultValue(formula, options.defaults);
972
+ setValueByPath2(values, fieldPath, defaultValue);
973
+ setValueByPath2(itemData, formula.localFieldPath, defaultValue);
974
+ }
975
+ errors.push({
976
+ field: fieldPath,
977
+ expression: formula.expression,
978
+ error: "Formula returned undefined",
979
+ defaultUsed: options.useDefaults ?? false
980
+ });
981
+ continue;
982
+ }
983
+ setValueByPath2(values, fieldPath, result);
984
+ setValueByPath2(itemData, formula.localFieldPath, result);
985
+ } catch (error) {
986
+ if (options.useDefaults) {
987
+ const defaultValue = getDefaultValue(formula, options.defaults);
988
+ setValueByPath2(values, fieldPath, defaultValue);
989
+ setValueByPath2(itemData, formula.localFieldPath, defaultValue);
990
+ }
991
+ errors.push({
992
+ field: fieldPath,
993
+ expression: formula.expression,
994
+ error: error instanceof Error ? error.message : String(error),
995
+ defaultUsed: options.useDefaults ?? false
996
+ });
997
+ }
998
+ }
999
+ return errors;
1000
+ }
1001
+ function getParentPath2(fieldPath) {
1002
+ const lastDotIndex = fieldPath.lastIndexOf(".");
1003
+ if (lastDotIndex === -1) {
1004
+ return "";
1005
+ }
1006
+ return fieldPath.substring(0, lastDotIndex);
1007
+ }
1008
+ function getValueByPath2(obj, path) {
1009
+ const segments = path.split(".");
1010
+ let current = obj;
1011
+ for (const segment of segments) {
1012
+ if (current === null || current === void 0) {
1013
+ return void 0;
1014
+ }
1015
+ current = current[segment];
1016
+ }
1017
+ return current;
1018
+ }
1019
+ function isSafeKey(key) {
1020
+ return key !== "__proto__";
1021
+ }
1022
+ function setValueByPath2(obj, path, value) {
1023
+ const segments = path.split(".");
1024
+ let current = obj;
1025
+ for (let i = 0; i < segments.length - 1; i++) {
1026
+ const segment = segments[i];
1027
+ if (!isSafeKey(segment)) {
1028
+ return;
1029
+ }
1030
+ if (!(segment in current)) {
1031
+ current[segment] = {};
1032
+ }
1033
+ current = current[segment];
1034
+ }
1035
+ const lastSegment = segments.at(-1);
1036
+ if (!isSafeKey(lastSegment)) {
1037
+ return;
1038
+ }
1039
+ current[lastSegment] = value;
1040
+ }
1041
+ function getDefaultValue(formula, defaults) {
1042
+ if (defaults && formula.fieldName in defaults) {
1043
+ return defaults[formula.fieldName];
1044
+ }
1045
+ switch (formula.fieldType) {
1046
+ case "number":
1047
+ return 0;
1048
+ case "string":
1049
+ return "";
1050
+ case "boolean":
1051
+ return false;
1052
+ default:
1053
+ return null;
1054
+ }
1055
+ }
1056
+ function createError(formula, error, defaultUsed) {
1057
+ return {
1058
+ field: formula.fieldName,
1059
+ expression: formula.expression,
1060
+ error,
1061
+ defaultUsed
1062
+ };
1063
+ }
1064
+
572
1065
  // src/lib/getDBJsonPathByJsonSchemaStore.ts
573
1066
  var getDBJsonPathByJsonSchemaStore = (store) => {
574
1067
  let node = store;
@@ -822,6 +1315,6 @@ var SchemaTable = class {
822
1315
  }
823
1316
  };
824
1317
 
825
- export { SchemaTable, VALIDATE_JSON_FIELD_NAME_ERROR_MESSAGE, applyAddPatch, applyMovePatch, applyRemovePatch, applyReplacePatch, computeValueDiff, convertJsonPathToSchemaPath, convertSchemaPathToJsonPath, createJsonObjectSchemaStore, createJsonSchemaStore, createPrimitiveStoreBySchema, deepEqual, getDBJsonPathByJsonSchemaStore, getForeignKeyPatchesFromSchema, getForeignKeysFromSchema, getForeignKeysFromValue, getInvalidFieldNamesInSchema, getJsonSchemaStoreByPath, getJsonValueStoreByPath, getParentForPath, getPathByStore, getValueByPath, hasPath, parsePath, pluginRefs, replaceForeignKeyValue, resolveRefs, saveSharedFields, setValueByPath, traverseStore, traverseValue, validateJsonFieldName };
826
- //# sourceMappingURL=chunk-4BIANIHT.js.map
827
- //# sourceMappingURL=chunk-4BIANIHT.js.map
1318
+ export { SchemaTable, VALIDATE_JSON_FIELD_NAME_ERROR_MESSAGE, applyAddPatch, applyMovePatch, applyRemovePatch, applyReplacePatch, computeValueDiff, convertJsonPathToSchemaPath, convertSchemaPathToJsonPath, createJsonObjectSchemaStore, createJsonSchemaStore, createPrimitiveStoreBySchema, deepEqual, evaluateFormulas, extractSchemaFormulas, getDBJsonPathByJsonSchemaStore, getForeignKeyPatchesFromSchema, getForeignKeysFromSchema, getForeignKeysFromValue, getInvalidFieldNamesInSchema, getJsonSchemaStoreByPath, getJsonValueStoreByPath, getParentForPath, getPathByStore, getValueByPath, hasPath, parsePath, pluginRefs, prepareFormulas, replaceForeignKeyValue, resolveRefs, saveSharedFields, setValueByPath, traverseStore, traverseValue, validateFormulaAgainstSchema, validateJsonFieldName, validateSchemaFormulas };
1319
+ //# sourceMappingURL=chunk-YSVBLK6R.js.map
1320
+ //# sourceMappingURL=chunk-YSVBLK6R.js.map