@stndrds/schema 1.0.0-alpha.112 → 1.0.0-alpha.113

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/dist/index.js CHANGED
@@ -22,6 +22,7 @@ require('./chunk-P2BE2A2G.js');
22
22
  var chunkT225DB55_js = require('./chunk-T225DB55.js');
23
23
  var constants = require('@stndrds/constants');
24
24
  var z3 = require('zod');
25
+ var exprEval = require('expr-eval');
25
26
 
26
27
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
27
28
 
@@ -4842,6 +4843,335 @@ var FormRegistry = class {
4842
4843
  }
4843
4844
  };
4844
4845
  var formRegistry = new FormRegistry();
4846
+ var DOS_PROTECTION_LIMITS = {
4847
+ MAX_EXPRESSION_LENGTH: 1e4,
4848
+ MAX_TOKEN_COUNT: 1e3,
4849
+ MAX_NESTING_DEPTH: 50
4850
+ };
4851
+ function createFormulaParser() {
4852
+ const parser = new exprEval.Parser();
4853
+ parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
4854
+ parser.functions.AND = (...args) => args.every(Boolean);
4855
+ parser.functions.OR = (...args) => args.some(Boolean);
4856
+ parser.functions.NOT = (value) => !value;
4857
+ parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
4858
+ parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
4859
+ parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
4860
+ parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
4861
+ parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
4862
+ parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
4863
+ parser.functions.TRIM = (value) => String(value ?? "").trim();
4864
+ parser.functions.LENGTH = (value) => String(value ?? "").length;
4865
+ parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
4866
+ parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
4867
+ parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").split(String(search)).join(replacement);
4868
+ parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
4869
+ parser.functions.ROUND = (value, decimals = 0) => {
4870
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4871
+ const factor = 10 ** decimals;
4872
+ return Math.round(value * factor) / factor;
4873
+ };
4874
+ parser.functions.FLOOR = (value) => {
4875
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4876
+ return Math.floor(value);
4877
+ };
4878
+ parser.functions.CEIL = (value) => {
4879
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4880
+ return Math.ceil(value);
4881
+ };
4882
+ parser.functions.ABS = (value) => {
4883
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4884
+ return Math.abs(value);
4885
+ };
4886
+ parser.functions.MIN = (...args) => {
4887
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
4888
+ return nums.length > 0 ? Math.min(...nums) : null;
4889
+ };
4890
+ parser.functions.MAX = (...args) => {
4891
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
4892
+ return nums.length > 0 ? Math.max(...nums) : null;
4893
+ };
4894
+ parser.functions.POW = (base, exponent) => {
4895
+ if (typeof base !== "number" || typeof exponent !== "number") return null;
4896
+ if (exponent > 1e3 || exponent < -1e3) return null;
4897
+ return base ** exponent;
4898
+ };
4899
+ parser.functions.MOD = (a, b) => {
4900
+ if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
4901
+ return a % b;
4902
+ };
4903
+ parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
4904
+ parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4905
+ parser.functions.YEAR = (value) => {
4906
+ const date2 = parseDate(value);
4907
+ return date2 ? date2.getFullYear() : null;
4908
+ };
4909
+ parser.functions.MONTH = (value) => {
4910
+ const date2 = parseDate(value);
4911
+ return date2 ? date2.getMonth() + 1 : null;
4912
+ };
4913
+ parser.functions.DAY = (value) => {
4914
+ const date2 = parseDate(value);
4915
+ return date2 ? date2.getDate() : null;
4916
+ };
4917
+ parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
4918
+ const d1 = parseDate(date1);
4919
+ const d2 = parseDate(date2);
4920
+ if (d1 === null || d2 === null) return null;
4921
+ const diffMs = d1.getTime() - d2.getTime();
4922
+ const MS_PER_DAY = 1e3 * 60 * 60 * 24;
4923
+ const conversions = {
4924
+ years: MS_PER_DAY * 365,
4925
+ months: MS_PER_DAY * 30,
4926
+ weeks: MS_PER_DAY * 7,
4927
+ days: MS_PER_DAY,
4928
+ hours: 1e3 * 60 * 60,
4929
+ minutes: 1e3 * 60
4930
+ };
4931
+ const divisor = conversions[unit] ?? MS_PER_DAY;
4932
+ return diffMs / divisor;
4933
+ };
4934
+ return parser;
4935
+ }
4936
+ function parseDate(value) {
4937
+ if (!value) return null;
4938
+ if (value instanceof Date) return value;
4939
+ if (typeof value === "string" || typeof value === "number") {
4940
+ const date2 = new Date(value);
4941
+ return Number.isNaN(date2.getTime()) ? null : date2;
4942
+ }
4943
+ return null;
4944
+ }
4945
+ var formulaParser = createFormulaParser();
4946
+ function validateFormulaComplexity(expression) {
4947
+ if (expression.length > DOS_PROTECTION_LIMITS.MAX_EXPRESSION_LENGTH) {
4948
+ return {
4949
+ valid: false,
4950
+ error: `Expression too long (${expression.length} chars, max ${DOS_PROTECTION_LIMITS.MAX_EXPRESSION_LENGTH})`
4951
+ };
4952
+ }
4953
+ const nestingDepth = calculateNestingDepth(expression);
4954
+ if (nestingDepth > DOS_PROTECTION_LIMITS.MAX_NESTING_DEPTH) {
4955
+ return {
4956
+ valid: false,
4957
+ error: `Expression too deeply nested (${nestingDepth} levels, max ${DOS_PROTECTION_LIMITS.MAX_NESTING_DEPTH})`
4958
+ };
4959
+ }
4960
+ try {
4961
+ formulaParser.parse(expression);
4962
+ const tokenCountApprox = estimateTokenCount(expression);
4963
+ if (tokenCountApprox > DOS_PROTECTION_LIMITS.MAX_TOKEN_COUNT) {
4964
+ return {
4965
+ valid: false,
4966
+ error: `Expression too complex (approx ${tokenCountApprox} tokens, max ${DOS_PROTECTION_LIMITS.MAX_TOKEN_COUNT})`
4967
+ };
4968
+ }
4969
+ return { valid: true };
4970
+ } catch {
4971
+ return { valid: true };
4972
+ }
4973
+ }
4974
+ function estimateTokenCount(expression) {
4975
+ const tokens = expression.match(/-?\d+\.?\d*|[a-zA-Z_][a-zA-Z0-9_]*|[+\-*/(),.><=!&|]+|'(?:[^'\\]|\\.)*'/g) ?? [];
4976
+ return tokens.length;
4977
+ }
4978
+ function calculateNestingDepth(expression) {
4979
+ let maxDepth = 0;
4980
+ let currentDepth = 0;
4981
+ for (const char of expression) {
4982
+ if (char === "(") {
4983
+ currentDepth++;
4984
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
4985
+ } else if (char === ")") {
4986
+ currentDepth--;
4987
+ }
4988
+ }
4989
+ return maxDepth;
4990
+ }
4991
+ function evaluateFormula(expression, values) {
4992
+ try {
4993
+ const complexityCheck = validateFormulaComplexity(expression);
4994
+ if (!complexityCheck.valid) return null;
4995
+ const parsed = formulaParser.parse(expression);
4996
+ return parsed.evaluate(values);
4997
+ } catch {
4998
+ return null;
4999
+ }
5000
+ }
5001
+ function evaluateFormulaWithResult(expression, values) {
5002
+ try {
5003
+ const complexityCheck = validateFormulaComplexity(expression);
5004
+ if (!complexityCheck.valid) {
5005
+ return { value: null, error: complexityCheck.error };
5006
+ }
5007
+ const parsed = formulaParser.parse(expression);
5008
+ const value = parsed.evaluate(values);
5009
+ return { value };
5010
+ } catch (error) {
5011
+ return { value: null, error: getErrorMessage(error) };
5012
+ }
5013
+ }
5014
+ function formatFormulaResult(value, returnType, decimals) {
5015
+ if (value === null || value === void 0) return null;
5016
+ switch (returnType) {
5017
+ case "number": {
5018
+ const num = typeof value === "number" ? value : Number(value);
5019
+ if (Number.isNaN(num)) return null;
5020
+ return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
5021
+ }
5022
+ case "boolean":
5023
+ return Boolean(value);
5024
+ case "date":
5025
+ if (value instanceof Date) return value.toISOString();
5026
+ if (typeof value === "string") return value;
5027
+ return null;
5028
+ case "text":
5029
+ return String(value);
5030
+ }
5031
+ }
5032
+ function evaluateFormulaAttribute(attr, values) {
5033
+ const raw = evaluateFormula(attr.expression, values);
5034
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
5035
+ }
5036
+ function validateFormulaExpression(expression) {
5037
+ try {
5038
+ const complexityCheck = validateFormulaComplexity(expression);
5039
+ if (!complexityCheck.valid) return complexityCheck;
5040
+ formulaParser.parse(expression);
5041
+ return { valid: true };
5042
+ } catch (error) {
5043
+ return { valid: false, error: getErrorMessage(error) };
5044
+ }
5045
+ }
5046
+ function extractFormulaVariables(expression) {
5047
+ try {
5048
+ const parsed = formulaParser.parse(expression);
5049
+ return parsed.variables();
5050
+ } catch {
5051
+ return [];
5052
+ }
5053
+ }
5054
+ var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
5055
+ function extractRelationReferences(expression) {
5056
+ const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
5057
+ const matches = expression.matchAll(regex);
5058
+ return [...matches].map((m) => m[0]);
5059
+ }
5060
+ function extractRelationNames(expression) {
5061
+ const refs = extractRelationReferences(expression);
5062
+ const names = refs.map((ref) => ref.split(".")[0]);
5063
+ return [...new Set(names)];
5064
+ }
5065
+ function hasRelationReferences(expression) {
5066
+ const regex = new RegExp(RELATION_REF_PATTERN.source);
5067
+ return regex.test(expression);
5068
+ }
5069
+ function flattenRelationsForEval(resolvedRelations) {
5070
+ const result = {};
5071
+ for (const [relationName, values] of Object.entries(resolvedRelations)) {
5072
+ result[relationName] = { ...values };
5073
+ }
5074
+ return result;
5075
+ }
5076
+
5077
+ // src/formula/path-parser.ts
5078
+ var InvalidPathError = class extends Error {
5079
+ constructor(path, segment, reason) {
5080
+ super(`Invalid path "${path}" at "${segment}": ${reason}`);
5081
+ this.path = path;
5082
+ this.segment = segment;
5083
+ this.reason = reason;
5084
+ this.name = "InvalidPathError";
5085
+ }
5086
+ };
5087
+ var MaxDepthExceededError = class extends Error {
5088
+ constructor(path, maxDepth) {
5089
+ super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
5090
+ this.path = path;
5091
+ this.maxDepth = maxDepth;
5092
+ this.name = "MaxDepthExceededError";
5093
+ }
5094
+ };
5095
+ async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
5096
+ const segments = path.split(".");
5097
+ if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
5098
+ throw new InvalidPathError(path, path, "Path cannot be empty");
5099
+ }
5100
+ if (segments.length > maxDepth) {
5101
+ throw new MaxDepthExceededError(path, maxDepth);
5102
+ }
5103
+ const result = [];
5104
+ let currentSchema = startSchema;
5105
+ for (let i = 0; i < segments.length; i++) {
5106
+ const segmentName = segments[i];
5107
+ const isLastSegment = i === segments.length - 1;
5108
+ const attr = currentSchema.attributes.find((a) => a.name === segmentName);
5109
+ if (!attr) {
5110
+ throw new InvalidPathError(
5111
+ path,
5112
+ segmentName,
5113
+ `Attribute "${segmentName}" not found in object "${currentSchema.name}"`
5114
+ );
5115
+ }
5116
+ if (attr.type === "relation") {
5117
+ const relationAttr = attr;
5118
+ const targetObject = relationAttr.targets[0]?.object;
5119
+ if (!targetObject) {
5120
+ throw new InvalidPathError(path, segmentName, "Relation has no target object");
5121
+ }
5122
+ result.push({
5123
+ name: segmentName,
5124
+ type: "relation",
5125
+ cardinality: relationAttr.cardinality,
5126
+ targetObject
5127
+ });
5128
+ if (!isLastSegment) {
5129
+ const nextSchema = await getSchema(targetObject);
5130
+ if (!nextSchema) {
5131
+ throw new InvalidPathError(
5132
+ path,
5133
+ segmentName,
5134
+ `Target object "${targetObject}" schema not found`
5135
+ );
5136
+ }
5137
+ currentSchema = nextSchema;
5138
+ }
5139
+ } else {
5140
+ if (!isLastSegment) {
5141
+ throw new InvalidPathError(
5142
+ path,
5143
+ segmentName,
5144
+ `"${segmentName}" is not a relation but has segments after it`
5145
+ );
5146
+ }
5147
+ result.push({ name: segmentName, type: "attribute" });
5148
+ }
5149
+ }
5150
+ return result;
5151
+ }
5152
+ async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
5153
+ try {
5154
+ await parsePath(path, startSchema, getSchema, maxDepth);
5155
+ return true;
5156
+ } catch {
5157
+ return false;
5158
+ }
5159
+ }
5160
+ function pathHasManyCardinality(segments) {
5161
+ return segments.some((s) => s.type === "relation" && s.cardinality === "many");
5162
+ }
5163
+ function getPathDepth(segments) {
5164
+ return segments.filter((s) => s.type === "relation").length;
5165
+ }
5166
+ function getTargetAttributeName(path) {
5167
+ const parts = path.split(".");
5168
+ return parts[parts.length - 1];
5169
+ }
5170
+ function getRelationPath(path) {
5171
+ const parts = path.split(".");
5172
+ if (parts.length <= 1) return null;
5173
+ return parts.slice(0, -1).join(".");
5174
+ }
4845
5175
 
4846
5176
  Object.defineProperty(exports, "asTenantId", {
4847
5177
  enumerable: true,
@@ -4923,8 +5253,10 @@ exports.FormRowBuilder = FormRowBuilder;
4923
5253
  exports.FormStepBuilder = FormStepBuilder;
4924
5254
  exports.GroupBuilder = GroupBuilder;
4925
5255
  exports.IDENTITY_PROPERTIES = IDENTITY_PROPERTIES;
5256
+ exports.InvalidPathError = InvalidPathError;
4926
5257
  exports.ListViewBuilder = ListViewBuilder;
4927
5258
  exports.ListViewTabConfigBuilder = ListViewTabConfigBuilder;
5259
+ exports.MaxDepthExceededError = MaxDepthExceededError;
4928
5260
  exports.MemoryNotFoundError = MemoryNotFoundError;
4929
5261
  exports.NO_VALUE_OPERATORS = NO_VALUE_OPERATORS;
4930
5262
  exports.NoopGeocodingAdapter = NoopGeocodingAdapter;
@@ -4971,21 +5303,33 @@ exports.currency = currency;
4971
5303
  exports.date = date;
4972
5304
  exports.detailView = detailView;
4973
5305
  exports.document = document;
5306
+ exports.evaluateFormula = evaluateFormula;
5307
+ exports.evaluateFormulaAttribute = evaluateFormulaAttribute;
5308
+ exports.evaluateFormulaWithResult = evaluateFormulaWithResult;
4974
5309
  exports.extractAttributeNames = extractAttributeNames;
5310
+ exports.extractFormulaVariables = extractFormulaVariables;
5311
+ exports.extractRelationNames = extractRelationNames;
5312
+ exports.extractRelationReferences = extractRelationReferences;
4975
5313
  exports.file = file;
4976
5314
  exports.flagRegistry = flagRegistry;
5315
+ exports.flattenRelationsForEval = flattenRelationsForEval;
4977
5316
  exports.form = form;
4978
5317
  exports.formRegistry = formRegistry;
4979
5318
  exports.formatAttributeValue = formatAttributeValue;
5319
+ exports.formatFormulaResult = formatFormulaResult;
4980
5320
  exports.formula = formula;
4981
5321
  exports.generateDefaultDetailView = generateDefaultDetailView;
4982
5322
  exports.generateDefaultListView = generateDefaultListView;
4983
5323
  exports.getActiveTab = getActiveTab;
4984
5324
  exports.getErrorMessage = getErrorMessage;
5325
+ exports.getPathDepth = getPathDepth;
5326
+ exports.getRelationPath = getRelationPath;
4985
5327
  exports.getRollupFilterOperators = getRollupFilterOperators;
4986
5328
  exports.getSystemAttributeList = getSystemAttributeList;
5329
+ exports.getTargetAttributeName = getTargetAttributeName;
4987
5330
  exports.group = group;
4988
5331
  exports.hasOptions = hasOptions;
5332
+ exports.hasRelationReferences = hasRelationReferences;
4989
5333
  exports.inferInverseCardinality = inferInverseCardinality;
4990
5334
  exports.isAttributeInUseError = isAttributeInUseError;
4991
5335
  exports.isAttributeSortable = isAttributeSortable;
@@ -5021,6 +5365,8 @@ exports.multiselect = multiselect;
5021
5365
  exports.number = number;
5022
5366
  exports.numberFlag = numberFlag;
5023
5367
  exports.object = object;
5368
+ exports.parsePath = parsePath;
5369
+ exports.pathHasManyCardinality = pathHasManyCardinality;
5024
5370
  exports.phone = phone;
5025
5371
  exports.rating = rating;
5026
5372
  exports.registry = registry;
@@ -5038,4 +5384,6 @@ exports.textarea = textarea;
5038
5384
  exports.toUndefinedIfEmpty = toUndefinedIfEmpty;
5039
5385
  exports.user = user;
5040
5386
  exports.validateAttributeName = validateAttributeName;
5387
+ exports.validateFormulaExpression = validateFormulaExpression;
5388
+ exports.validatePath = validatePath;
5041
5389
  exports.viewRegistry = viewRegistry;