@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.mjs CHANGED
@@ -22,6 +22,7 @@ import './chunk-R4CJHCC4.mjs';
22
22
  export { DEFAULT_VALIDATION_MESSAGES, formatZodErrors } from './chunk-UQVW4KPP.mjs';
23
23
  import { ICONS, getCountryDisplayNameByIso3 } from '@stndrds/constants';
24
24
  import z3, { z } from 'zod';
25
+ import { Parser } from 'expr-eval';
25
26
 
26
27
  // src/types/attribute-protection.ts
27
28
  var IDENTITY_PROPERTIES = ["name", "type"];
@@ -4838,5 +4839,334 @@ var FormRegistry = class {
4838
4839
  }
4839
4840
  };
4840
4841
  var formRegistry = new FormRegistry();
4842
+ var DOS_PROTECTION_LIMITS = {
4843
+ MAX_EXPRESSION_LENGTH: 1e4,
4844
+ MAX_TOKEN_COUNT: 1e3,
4845
+ MAX_NESTING_DEPTH: 50
4846
+ };
4847
+ function createFormulaParser() {
4848
+ const parser = new Parser();
4849
+ parser.functions.IF = (condition, thenValue, elseValue) => condition ? thenValue : elseValue;
4850
+ parser.functions.AND = (...args) => args.every(Boolean);
4851
+ parser.functions.OR = (...args) => args.some(Boolean);
4852
+ parser.functions.NOT = (value) => !value;
4853
+ parser.functions.EMPTY = (value) => value === null || value === void 0 || value === "";
4854
+ parser.functions.COALESCE = (...args) => args.find((a) => a != null) ?? null;
4855
+ parser.functions.DEFAULT = (value, defaultValue) => value == null || value === "" ? defaultValue : value;
4856
+ parser.functions.CONCAT = (...args) => args.filter((a) => a != null).map(String).join("");
4857
+ parser.functions.UPPER = (value) => String(value ?? "").toUpperCase();
4858
+ parser.functions.LOWER = (value) => String(value ?? "").toLowerCase();
4859
+ parser.functions.TRIM = (value) => String(value ?? "").trim();
4860
+ parser.functions.LENGTH = (value) => String(value ?? "").length;
4861
+ parser.functions.LEFT = (value, count) => String(value ?? "").slice(0, count);
4862
+ parser.functions.RIGHT = (value, count) => String(value ?? "").slice(-count);
4863
+ parser.functions.REPLACE = (value, search, replacement) => String(value ?? "").split(String(search)).join(replacement);
4864
+ parser.functions.CONTAINS = (value, search) => String(value ?? "").toLowerCase().includes(String(search).toLowerCase());
4865
+ parser.functions.ROUND = (value, decimals = 0) => {
4866
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4867
+ const factor = 10 ** decimals;
4868
+ return Math.round(value * factor) / factor;
4869
+ };
4870
+ parser.functions.FLOOR = (value) => {
4871
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4872
+ return Math.floor(value);
4873
+ };
4874
+ parser.functions.CEIL = (value) => {
4875
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4876
+ return Math.ceil(value);
4877
+ };
4878
+ parser.functions.ABS = (value) => {
4879
+ if (typeof value !== "number" || Number.isNaN(value)) return null;
4880
+ return Math.abs(value);
4881
+ };
4882
+ parser.functions.MIN = (...args) => {
4883
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
4884
+ return nums.length > 0 ? Math.min(...nums) : null;
4885
+ };
4886
+ parser.functions.MAX = (...args) => {
4887
+ const nums = args.filter((n) => typeof n === "number" && !Number.isNaN(n));
4888
+ return nums.length > 0 ? Math.max(...nums) : null;
4889
+ };
4890
+ parser.functions.POW = (base, exponent) => {
4891
+ if (typeof base !== "number" || typeof exponent !== "number") return null;
4892
+ if (exponent > 1e3 || exponent < -1e3) return null;
4893
+ return base ** exponent;
4894
+ };
4895
+ parser.functions.MOD = (a, b) => {
4896
+ if (typeof a !== "number" || typeof b !== "number" || b === 0) return null;
4897
+ return a % b;
4898
+ };
4899
+ parser.functions.NOW = () => (/* @__PURE__ */ new Date()).toISOString();
4900
+ parser.functions.TODAY = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4901
+ parser.functions.YEAR = (value) => {
4902
+ const date2 = parseDate(value);
4903
+ return date2 ? date2.getFullYear() : null;
4904
+ };
4905
+ parser.functions.MONTH = (value) => {
4906
+ const date2 = parseDate(value);
4907
+ return date2 ? date2.getMonth() + 1 : null;
4908
+ };
4909
+ parser.functions.DAY = (value) => {
4910
+ const date2 = parseDate(value);
4911
+ return date2 ? date2.getDate() : null;
4912
+ };
4913
+ parser.functions.DATE_DIFF = (date1, date2, unit = "days") => {
4914
+ const d1 = parseDate(date1);
4915
+ const d2 = parseDate(date2);
4916
+ if (d1 === null || d2 === null) return null;
4917
+ const diffMs = d1.getTime() - d2.getTime();
4918
+ const MS_PER_DAY = 1e3 * 60 * 60 * 24;
4919
+ const conversions = {
4920
+ years: MS_PER_DAY * 365,
4921
+ months: MS_PER_DAY * 30,
4922
+ weeks: MS_PER_DAY * 7,
4923
+ days: MS_PER_DAY,
4924
+ hours: 1e3 * 60 * 60,
4925
+ minutes: 1e3 * 60
4926
+ };
4927
+ const divisor = conversions[unit] ?? MS_PER_DAY;
4928
+ return diffMs / divisor;
4929
+ };
4930
+ return parser;
4931
+ }
4932
+ function parseDate(value) {
4933
+ if (!value) return null;
4934
+ if (value instanceof Date) return value;
4935
+ if (typeof value === "string" || typeof value === "number") {
4936
+ const date2 = new Date(value);
4937
+ return Number.isNaN(date2.getTime()) ? null : date2;
4938
+ }
4939
+ return null;
4940
+ }
4941
+ var formulaParser = createFormulaParser();
4942
+ function validateFormulaComplexity(expression) {
4943
+ if (expression.length > DOS_PROTECTION_LIMITS.MAX_EXPRESSION_LENGTH) {
4944
+ return {
4945
+ valid: false,
4946
+ error: `Expression too long (${expression.length} chars, max ${DOS_PROTECTION_LIMITS.MAX_EXPRESSION_LENGTH})`
4947
+ };
4948
+ }
4949
+ const nestingDepth = calculateNestingDepth(expression);
4950
+ if (nestingDepth > DOS_PROTECTION_LIMITS.MAX_NESTING_DEPTH) {
4951
+ return {
4952
+ valid: false,
4953
+ error: `Expression too deeply nested (${nestingDepth} levels, max ${DOS_PROTECTION_LIMITS.MAX_NESTING_DEPTH})`
4954
+ };
4955
+ }
4956
+ try {
4957
+ formulaParser.parse(expression);
4958
+ const tokenCountApprox = estimateTokenCount(expression);
4959
+ if (tokenCountApprox > DOS_PROTECTION_LIMITS.MAX_TOKEN_COUNT) {
4960
+ return {
4961
+ valid: false,
4962
+ error: `Expression too complex (approx ${tokenCountApprox} tokens, max ${DOS_PROTECTION_LIMITS.MAX_TOKEN_COUNT})`
4963
+ };
4964
+ }
4965
+ return { valid: true };
4966
+ } catch {
4967
+ return { valid: true };
4968
+ }
4969
+ }
4970
+ function estimateTokenCount(expression) {
4971
+ const tokens = expression.match(/-?\d+\.?\d*|[a-zA-Z_][a-zA-Z0-9_]*|[+\-*/(),.><=!&|]+|'(?:[^'\\]|\\.)*'/g) ?? [];
4972
+ return tokens.length;
4973
+ }
4974
+ function calculateNestingDepth(expression) {
4975
+ let maxDepth = 0;
4976
+ let currentDepth = 0;
4977
+ for (const char of expression) {
4978
+ if (char === "(") {
4979
+ currentDepth++;
4980
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
4981
+ } else if (char === ")") {
4982
+ currentDepth--;
4983
+ }
4984
+ }
4985
+ return maxDepth;
4986
+ }
4987
+ function evaluateFormula(expression, values) {
4988
+ try {
4989
+ const complexityCheck = validateFormulaComplexity(expression);
4990
+ if (!complexityCheck.valid) return null;
4991
+ const parsed = formulaParser.parse(expression);
4992
+ return parsed.evaluate(values);
4993
+ } catch {
4994
+ return null;
4995
+ }
4996
+ }
4997
+ function evaluateFormulaWithResult(expression, values) {
4998
+ try {
4999
+ const complexityCheck = validateFormulaComplexity(expression);
5000
+ if (!complexityCheck.valid) {
5001
+ return { value: null, error: complexityCheck.error };
5002
+ }
5003
+ const parsed = formulaParser.parse(expression);
5004
+ const value = parsed.evaluate(values);
5005
+ return { value };
5006
+ } catch (error) {
5007
+ return { value: null, error: getErrorMessage(error) };
5008
+ }
5009
+ }
5010
+ function formatFormulaResult(value, returnType, decimals) {
5011
+ if (value === null || value === void 0) return null;
5012
+ switch (returnType) {
5013
+ case "number": {
5014
+ const num = typeof value === "number" ? value : Number(value);
5015
+ if (Number.isNaN(num)) return null;
5016
+ return decimals !== void 0 ? Number(num.toFixed(decimals)) : num;
5017
+ }
5018
+ case "boolean":
5019
+ return Boolean(value);
5020
+ case "date":
5021
+ if (value instanceof Date) return value.toISOString();
5022
+ if (typeof value === "string") return value;
5023
+ return null;
5024
+ case "text":
5025
+ return String(value);
5026
+ }
5027
+ }
5028
+ function evaluateFormulaAttribute(attr, values) {
5029
+ const raw = evaluateFormula(attr.expression, values);
5030
+ return formatFormulaResult(raw, attr.returnType, attr.decimals);
5031
+ }
5032
+ function validateFormulaExpression(expression) {
5033
+ try {
5034
+ const complexityCheck = validateFormulaComplexity(expression);
5035
+ if (!complexityCheck.valid) return complexityCheck;
5036
+ formulaParser.parse(expression);
5037
+ return { valid: true };
5038
+ } catch (error) {
5039
+ return { valid: false, error: getErrorMessage(error) };
5040
+ }
5041
+ }
5042
+ function extractFormulaVariables(expression) {
5043
+ try {
5044
+ const parsed = formulaParser.parse(expression);
5045
+ return parsed.variables();
5046
+ } catch {
5047
+ return [];
5048
+ }
5049
+ }
5050
+ var RELATION_REF_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
5051
+ function extractRelationReferences(expression) {
5052
+ const regex = new RegExp(RELATION_REF_PATTERN.source, "g");
5053
+ const matches = expression.matchAll(regex);
5054
+ return [...matches].map((m) => m[0]);
5055
+ }
5056
+ function extractRelationNames(expression) {
5057
+ const refs = extractRelationReferences(expression);
5058
+ const names = refs.map((ref) => ref.split(".")[0]);
5059
+ return [...new Set(names)];
5060
+ }
5061
+ function hasRelationReferences(expression) {
5062
+ const regex = new RegExp(RELATION_REF_PATTERN.source);
5063
+ return regex.test(expression);
5064
+ }
5065
+ function flattenRelationsForEval(resolvedRelations) {
5066
+ const result = {};
5067
+ for (const [relationName, values] of Object.entries(resolvedRelations)) {
5068
+ result[relationName] = { ...values };
5069
+ }
5070
+ return result;
5071
+ }
5072
+
5073
+ // src/formula/path-parser.ts
5074
+ var InvalidPathError = class extends Error {
5075
+ constructor(path, segment, reason) {
5076
+ super(`Invalid path "${path}" at "${segment}": ${reason}`);
5077
+ this.path = path;
5078
+ this.segment = segment;
5079
+ this.reason = reason;
5080
+ this.name = "InvalidPathError";
5081
+ }
5082
+ };
5083
+ var MaxDepthExceededError = class extends Error {
5084
+ constructor(path, maxDepth) {
5085
+ super(`Path "${path}" exceeds maximum depth of ${maxDepth}`);
5086
+ this.path = path;
5087
+ this.maxDepth = maxDepth;
5088
+ this.name = "MaxDepthExceededError";
5089
+ }
5090
+ };
5091
+ async function parsePath(path, startSchema, getSchema, maxDepth = 5) {
5092
+ const segments = path.split(".");
5093
+ if (segments.length === 0 || segments.length === 1 && segments[0] === "") {
5094
+ throw new InvalidPathError(path, path, "Path cannot be empty");
5095
+ }
5096
+ if (segments.length > maxDepth) {
5097
+ throw new MaxDepthExceededError(path, maxDepth);
5098
+ }
5099
+ const result = [];
5100
+ let currentSchema = startSchema;
5101
+ for (let i = 0; i < segments.length; i++) {
5102
+ const segmentName = segments[i];
5103
+ const isLastSegment = i === segments.length - 1;
5104
+ const attr = currentSchema.attributes.find((a) => a.name === segmentName);
5105
+ if (!attr) {
5106
+ throw new InvalidPathError(
5107
+ path,
5108
+ segmentName,
5109
+ `Attribute "${segmentName}" not found in object "${currentSchema.name}"`
5110
+ );
5111
+ }
5112
+ if (attr.type === "relation") {
5113
+ const relationAttr = attr;
5114
+ const targetObject = relationAttr.targets[0]?.object;
5115
+ if (!targetObject) {
5116
+ throw new InvalidPathError(path, segmentName, "Relation has no target object");
5117
+ }
5118
+ result.push({
5119
+ name: segmentName,
5120
+ type: "relation",
5121
+ cardinality: relationAttr.cardinality,
5122
+ targetObject
5123
+ });
5124
+ if (!isLastSegment) {
5125
+ const nextSchema = await getSchema(targetObject);
5126
+ if (!nextSchema) {
5127
+ throw new InvalidPathError(
5128
+ path,
5129
+ segmentName,
5130
+ `Target object "${targetObject}" schema not found`
5131
+ );
5132
+ }
5133
+ currentSchema = nextSchema;
5134
+ }
5135
+ } else {
5136
+ if (!isLastSegment) {
5137
+ throw new InvalidPathError(
5138
+ path,
5139
+ segmentName,
5140
+ `"${segmentName}" is not a relation but has segments after it`
5141
+ );
5142
+ }
5143
+ result.push({ name: segmentName, type: "attribute" });
5144
+ }
5145
+ }
5146
+ return result;
5147
+ }
5148
+ async function validatePath(path, startSchema, getSchema, maxDepth = 5) {
5149
+ try {
5150
+ await parsePath(path, startSchema, getSchema, maxDepth);
5151
+ return true;
5152
+ } catch {
5153
+ return false;
5154
+ }
5155
+ }
5156
+ function pathHasManyCardinality(segments) {
5157
+ return segments.some((s) => s.type === "relation" && s.cardinality === "many");
5158
+ }
5159
+ function getPathDepth(segments) {
5160
+ return segments.filter((s) => s.type === "relation").length;
5161
+ }
5162
+ function getTargetAttributeName(path) {
5163
+ const parts = path.split(".");
5164
+ return parts[parts.length - 1];
5165
+ }
5166
+ function getRelationPath(path) {
5167
+ const parts = path.split(".");
5168
+ if (parts.length <= 1) return null;
5169
+ return parts.slice(0, -1).join(".");
5170
+ }
4841
5171
 
4842
- export { ALL_ACTIONS, ALL_SYSTEM_RESOURCES, AccessDeniedError, ActivityTabConfig, AttributeInUseError, AttributeNotFoundError, BEHAVIOR_PROPERTIES, ConcurrentModificationError, CustomTabConfig, DB_COLUMN_FIELDS, DEFAULT_ROLES, DEFAULT_ROLE_DESCRIPTIONS, DEFAULT_ROLE_LABELS, DEFAULT_ROLE_PERMISSIONS, DetailViewBuilder, DocumentsTabConfig, DuplicateError, EMPTY_VALUE_PLACEHOLDER, FORM_FORBIDDEN_ATTRIBUTE_TYPES, FlagRegistry, FlagService, ForbiddenError, FormBuilder, FormRegistry, FormRowBuilder, FormStepBuilder, GroupBuilder, IDENTITY_PROPERTIES, ListViewBuilder, ListViewTabConfigBuilder, MemoryNotFoundError, NO_VALUE_OPERATORS, NoopGeocodingAdapter, NotFoundError, NotImplementedError, OPERATORS_BY_TYPE, ObjectBuilder, ObjectNotFoundError, ObjectReferencedError, PRESENTATION_PROPERTIES, PolicyViolationError, ProtectedResourceError, ProtectedRoleError, RELATION_TARGET_ANY, RESERVED_ATTRIBUTE_NAMES, RecordNotFoundError, RecordReferencedError, RelationGroupBuilder, RepositoryError, RichtextTabConfig, RoleNotFoundError, STANDARD_SCHEMA_VENDOR, SYSTEM_ATTRIBUTES, SYSTEM_FIELD_NAMES, SYSTEM_RESOURCES, SYSTEM_RESOURCE_LABELS, SchemaError, SchemaErrorCode, StorageError, SyncError, TabBuilder, TableTabConfig, USER_STATUSES, ValidationError, accessLevelToActions, actionsToAccessLevel, applyPipes, booleanFlag, checkbox, createFlagRegistry, createFlagService, createStandardSchemaProps, currency, date, detailView, document, extractAttributeNames, file, flagRegistry, form, formRegistry, formatAttributeValue, formula, generateDefaultDetailView, generateDefaultListView, getActiveTab, getErrorMessage, getRollupFilterOperators, getSystemAttributeList, group, hasOptions, inferInverseCardinality, isAttributeInUseError, isAttributeSortable, isBilateralRelation, isDefaultRole, isDetailView, isEmptyObject, isFieldGroup, isFormDefinition, isFormFieldsRow, isFormFreeFieldRef, isFormHeadingRow, isFormSeparatorRow, isFormSlotFieldRef, isFormTextRow, isLabelExpression, isListView, isNoValueOperator, isNotEmpty, isNotFoundError, isObjectReferencedError, isProtectedResourceError, isRecordReferencedError, isRelationGroup, isSchemaError, isStandardSchema, isUniversalRelation, isValidationError, jsonFlag, listView, location, multiselect, number, numberFlag, object, phone, rating, registry, relation, relationGroup, renderLabelExpression, resetViewToDefault, richtext, rollup, select, status, stringFlag, text, textarea, toUndefinedIfEmpty, user, validateAttributeName, viewRegistry };
5172
+ export { ALL_ACTIONS, ALL_SYSTEM_RESOURCES, AccessDeniedError, ActivityTabConfig, AttributeInUseError, AttributeNotFoundError, BEHAVIOR_PROPERTIES, ConcurrentModificationError, CustomTabConfig, DB_COLUMN_FIELDS, DEFAULT_ROLES, DEFAULT_ROLE_DESCRIPTIONS, DEFAULT_ROLE_LABELS, DEFAULT_ROLE_PERMISSIONS, DetailViewBuilder, DocumentsTabConfig, DuplicateError, EMPTY_VALUE_PLACEHOLDER, FORM_FORBIDDEN_ATTRIBUTE_TYPES, FlagRegistry, FlagService, ForbiddenError, FormBuilder, FormRegistry, FormRowBuilder, FormStepBuilder, GroupBuilder, IDENTITY_PROPERTIES, InvalidPathError, ListViewBuilder, ListViewTabConfigBuilder, MaxDepthExceededError, MemoryNotFoundError, NO_VALUE_OPERATORS, NoopGeocodingAdapter, NotFoundError, NotImplementedError, OPERATORS_BY_TYPE, ObjectBuilder, ObjectNotFoundError, ObjectReferencedError, PRESENTATION_PROPERTIES, PolicyViolationError, ProtectedResourceError, ProtectedRoleError, RELATION_TARGET_ANY, RESERVED_ATTRIBUTE_NAMES, RecordNotFoundError, RecordReferencedError, RelationGroupBuilder, RepositoryError, RichtextTabConfig, RoleNotFoundError, STANDARD_SCHEMA_VENDOR, SYSTEM_ATTRIBUTES, SYSTEM_FIELD_NAMES, SYSTEM_RESOURCES, SYSTEM_RESOURCE_LABELS, SchemaError, SchemaErrorCode, StorageError, SyncError, TabBuilder, TableTabConfig, USER_STATUSES, ValidationError, accessLevelToActions, actionsToAccessLevel, applyPipes, booleanFlag, checkbox, createFlagRegistry, createFlagService, createStandardSchemaProps, currency, date, detailView, document, evaluateFormula, evaluateFormulaAttribute, evaluateFormulaWithResult, extractAttributeNames, extractFormulaVariables, extractRelationNames, extractRelationReferences, file, flagRegistry, flattenRelationsForEval, form, formRegistry, formatAttributeValue, formatFormulaResult, formula, generateDefaultDetailView, generateDefaultListView, getActiveTab, getErrorMessage, getPathDepth, getRelationPath, getRollupFilterOperators, getSystemAttributeList, getTargetAttributeName, group, hasOptions, hasRelationReferences, inferInverseCardinality, isAttributeInUseError, isAttributeSortable, isBilateralRelation, isDefaultRole, isDetailView, isEmptyObject, isFieldGroup, isFormDefinition, isFormFieldsRow, isFormFreeFieldRef, isFormHeadingRow, isFormSeparatorRow, isFormSlotFieldRef, isFormTextRow, isLabelExpression, isListView, isNoValueOperator, isNotEmpty, isNotFoundError, isObjectReferencedError, isProtectedResourceError, isRecordReferencedError, isRelationGroup, isSchemaError, isStandardSchema, isUniversalRelation, isValidationError, jsonFlag, listView, location, multiselect, number, numberFlag, object, parsePath, pathHasManyCardinality, phone, rating, registry, relation, relationGroup, renderLabelExpression, resetViewToDefault, richtext, rollup, select, status, stringFlag, text, textarea, toUndefinedIfEmpty, user, validateAttributeName, validateFormulaExpression, validatePath, viewRegistry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stndrds/schema",
3
- "version": "1.0.0-alpha.112",
3
+ "version": "1.0.0-alpha.113",
4
4
  "description": "Standard schema definitions and utilities",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -128,9 +128,10 @@
128
128
  ],
129
129
  "dependencies": {
130
130
  "@standard-schema/spec": "^1.1.0",
131
+ "expr-eval": "^2.0.2",
131
132
  "libphonenumber-js": "^1.12.31",
132
133
  "zod": "^4.2.1",
133
- "@stndrds/constants": "1.0.0-alpha.112"
134
+ "@stndrds/constants": "1.0.0-alpha.113"
134
135
  },
135
136
  "devDependencies": {
136
137
  "@types/node": "^25.0.3",